phanerozoic commited on
Commit
9dc9d2f
·
1 Parent(s): 60d3c95

Consolidate the Python sources into twelve files with one-line docstrings, fold the demos into the playground, and delete the scripts that do not belong to the artifact

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +1 -1
  2. README.md +91 -123
  3. demos/README.md +0 -22
  4. demos/neural_attractor_factoring.py +0 -55
  5. demos/neural_attractor_nqueens.py +0 -90
  6. demos/neural_ca_loschmidt_echo.py +0 -55
  7. demos/neural_ca_reversible_cipher.py +0 -84
  8. demos/neural_computer8_collatz.py +0 -76
  9. demos/neural_computer8_euclid_gcd.py +0 -63
  10. demos/neural_computer8_self_modifying_sieve.py +0 -76
  11. demos/neural_matrix8_gpu_cpu_fleet.py +0 -73
  12. demos/neural_reflect_self_modifying_stack.py +0 -87
  13. demos/neural_reversible_counterfactual.py +0 -49
  14. demos/neural_rv32_machin_pi.py +0 -85
  15. demos/neural_rv32_neural_nets_via_neur.py +0 -184
  16. demos/neural_subleq8io_universal_constructor.py +0 -74
  17. demos/neural_tile_pascal_lucas.py +0 -42
  18. llm_integration/baseline.py +0 -221
  19. llm_integration/circuits.py +0 -320
  20. llm_integration/fitness.py +0 -218
  21. llm_integration/{model.py → llm.py} +1605 -244
  22. llm_integration/smollm2/analyze_smollm2.py +0 -232
  23. llm_integration/train.py +0 -955
  24. paper/README.md +34 -40
  25. paper/universal_construction_ternary.pdf +2 -2
  26. paper/universal_construction_ternary.tex +4 -4
  27. src/attractor.py +0 -261
  28. src/automata.py +1759 -0
  29. src/build.py +591 -599
  30. src/ca.py +0 -261
  31. src/canonical.py +0 -25
  32. src/check_c.py +0 -199
  33. src/check_codec.py +0 -128
  34. src/check_device.py +0 -118
  35. src/check_environment.py +0 -110
  36. src/check_family.py +0 -64
  37. src/check_family_gate.py +0 -70
  38. src/check_framing.py +0 -133
  39. src/check_host.py +0 -113
  40. src/check_host_family.py +0 -65
  41. src/check_host_fold.py +0 -73
  42. src/check_lev.py +0 -121
  43. src/check_optimal.py +0 -153
  44. src/check_rocq.py +0 -67
  45. src/check_sigma.py +0 -89
  46. src/check_steps.py +0 -169
  47. src/check_ternarity.py +0 -142
  48. src/constructor8.py +0 -580
  49. src/eval.py +1578 -150
  50. src/eval_all.py +0 -912
.gitignore CHANGED
@@ -21,4 +21,4 @@ rocq/.nia.cache
21
  rocq/extract/dump
22
  rocq/extract/*.cm*
23
  rocq/extract/*.o
24
- src/independent/.work/
 
21
  rocq/extract/dump
22
  rocq/extract/*.cm*
23
  rocq/extract/*.o
24
+ src/.work/
README.md CHANGED
@@ -99,14 +99,14 @@ for a, c in [(0, 0), (0, 1), (1, 0), (1, 1)]:
99
  Run the full circuit verification suite against any variant:
100
 
101
  ```bash
102
- python src/eval_all.py variants/ # 18 fitness variants (machines skipped)
103
- python src/eval_all.py neural_computer.safetensors # the canonical file
104
- python src/eval_all.py --cpu-program variants/ # also run an assembled
105
  # program through the
106
  # threshold-gated CPU
107
  ```
108
 
109
- `src/eval_all.py` reads each variant's manifest, runs a gate-level fitness suite (13,900–15,900 tests per variant covering Boolean, arithmetic, ALU, control, modular, error-detection, threshold, and float circuits, including end-to-end evaluation of the composed float pipelines from the shipped wiring metadata — see the Verification table), and optionally executes a small assembled program through a manifest-sized threshold CPU plus a chained 16- or 32-bit ALU sequence on wider variants.
110
 
111
  For an interactive walkthrough that exercises Boolean gates, the 8-bit ALU, mod-5 divisibility, and a CPU loop end-to-end:
112
 
@@ -115,15 +115,34 @@ python tools/play.py # 1 KB demo, runs in s
115
  python tools/play.py --model neural_computer.safetensors # 64 KB, slower
116
  ```
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  For end-to-end CPU validation (Fibonacci, sum 1..N, bubble sort, self-modifying JMP, all eight conditional jumps, CALL stack semantics, MUL cross-checked against repeated ADD, DIV cross-checked against repeated SUB, a bitwise AND/OR/XOR/SHL/SHR chain, and the architectural flag policy):
119
 
120
  ```bash
121
- python tools/test_cpu.py # default: 1 KB, ~2 s
122
- python tools/test_cpu.py --model neural_computer.safetensors # 64 KB canonical, ~2 min
123
- python tools/test_cpu.py --only fib,sum_n # subset of suite
124
  ```
125
 
126
- Each program is assembled by a small Python assembler (`tools/cpu_programs.py`) and run through the threshold-gated CPU; the driver verifies expected memory contents at HALT.
127
 
128
  ---
129
 
@@ -325,7 +344,7 @@ python src/build.py --bits 32 -a 6 --apply all # neural_computer32_addr6
325
  To regenerate every named variant in one pass:
326
 
327
  ```bash
328
- python tools/build_all.py
329
  ```
330
 
331
  This populates `variants/` with all 18 builds, quantizes each one to the smallest signed integer dtype that exactly represents its weights (~4× reduction in tensor data, with file size dominated by the safetensors header on the smaller profiles), verifies the strictly ternary weight invariant (`--ternary --strict`, so a build with any non-ternary weight fails loudly), stamps the `weight_quantization` metadata field, and runs `src/eval.py` on each as a sanity check.
@@ -333,11 +352,11 @@ This populates `variants/` with all 18 builds, quantizes each one to the smalles
333
  The quantizer is also available standalone:
334
 
335
  ```bash
336
- python src/quantize.py path/to/file.safetensors # in-place
337
- python src/quantize.py variants/ # whole directory
338
- python src/quantize.py model.safetensors -o quantized.safetensors
339
- python src/quantize.py file.safetensors --ternary # push toward {-1, 0, 1} weights
340
- python src/quantize.py file.safetensors --ternary --strict # error if any weight is non-ternary
341
  ```
342
 
343
  Every weight and bias tensor in the canonical model fits in `int8`. The eval pipeline promotes weights to `float32` on load, so integer storage is exact and transparent.
@@ -369,7 +388,7 @@ Every weight and bias tensor in the canonical model fits in `int8`. The eval pip
369
 
370
  The 8-bit arithmetic and ALU tests sample strategically; the circuits are constructed gate by gate, so the full 65,536-case sweep adds nothing. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
371
 
372
- `src/eval_all.py` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing evaluates the shipped weights; rebuilding is a separate step.** `python src/eval_all.py variants/` scores all 18 fitness variants against the shipped weights in about two minutes (~6 s each, the composed float netlists evaluated in `NetlistEvaluator`'s leveled mode) and cleanly skips the nine standalone machines. Rebuilding the models (`tools/build_all.py`, ~50 min for all 18) is a separate step, needed only when the circuit constructions in `src/build.py` change; routine verification never rebuilds. The batched evaluator is population-safe: every chained intermediate (carry, borrow, mux select) is computed per population slot, so `tools/prune_weights.py`'s parallel fitness screens are exact, with no slot-0 approximation anywhere.
373
 
374
  ---
375
 
@@ -405,7 +424,7 @@ Signed comparisons ride the unsigned bit-cascade with sign bits complemented thr
405
 
406
  ```bash
407
  python src/build.py --apply rv32 # build the file
408
- python src/quantize.py variants/neural_rv32.safetensors --ternary --strict
409
  python src/machines.py rv32 # eight-program lockstep suite
410
  python src/machines.py rv32-c # stock-compiler C, end to end
411
  ```
@@ -461,9 +480,9 @@ write is gated by NOT halt), so iterating past HALT is harmless.
461
  conductance mismatch (bit-exact through σ_G = 0.10).
462
 
463
  ```bash
464
- python src/matrix8.py build # compile + save variants/neural_matrix8.safetensors
465
- python src/matrix8.py verify # exhaustive equality vs the gate graph and integer reference
466
- python src/matrix8.py analog # margin measurement + read-noise + conductance-mismatch sweeps
467
  ```
468
 
469
  Because the transition is a fixed matrix stack proven equal to the gate-graph
@@ -508,8 +527,8 @@ correct assignment, but reaching it is NP-hard in general and not guaranteed
508
  within a fixed annealing schedule.
509
 
510
  ```bash
511
- python tools/build_attractor.py # compile a multiplier to variants/neural_attractor.safetensors
512
- python tools/test_attractor.py # forward eval, whole-network relaxation, factoring, SAT
513
  ```
514
 
515
  ---
@@ -549,9 +568,9 @@ Four checks establish the equality:
549
  closing the loop across two generations.
550
 
551
  ```bash
552
- python src/constructor8.py build # emit variants/neural_subleq8io.safetensors
553
- python src/constructor8.py verify # host soundness + codec round-trip + constructor on all 3 backends
554
- python src/constructor8.py self # full self-reproduction on the threshold matrices, then gen-2 boot
555
  ```
556
 
557
  A program running on the processor emits the exact tensor file that defines
@@ -562,12 +581,12 @@ weights and checked to the byte.
562
  `paper/` states and proves what this machine does, for a host serialized one
563
  unit at a time: `variants/neural_subleq8io_netlist.safetensors` stores the bias
564
  of every unit and every edge of the wiring, 17,598 units and 51,132 edges in
565
- 160,240 bytes, in the order `src/host_family.py` builds them, so the network the
566
  file denotes is the network the paper analyses, and parsing the file rebuilds
567
  it. That host is the member with eight-bit words of a family whose step has
568
  depth 13 for every word width, with a carry-lookahead subtractor and the
569
- operands fetched through the decode lines of the counter; `src/host_family.py`
570
- builds any member and `src/check_host_family.py` checks each against SUBLEQ
571
  on the evaluator in C. Separating construction from copying
572
  as von Neumann did, a 27-instruction program emits the serialization of the
573
  host, its initial memory and its own description tape; each generation is
@@ -575,9 +594,9 @@ instantiated from the bytes the last one emitted, by parsing that output,
575
  rebuilding the netlist from its first field, and building the evaluator from
576
  that netlist. It also shows where the restriction to {-1, 0, 1} is paid for,
577
  and that a fixed ternary network, the two-head machine of
578
- `src/twohead_net.py`, holds a stored network in its state and evaluates one
579
  record per step, which `rocq/` proves for every window of 2^α cells, bank and
580
- tape. The host itself, compiled by `src/stored_host.py` into 36,074 records of
581
  two inputs with records that load its memory, its tape device and a hand-over,
582
  and stored in the member with 4096 cells, runs the 27-instruction program on a
583
  recipe for its own records: it writes them into the idle bank, copies the recipe
@@ -587,20 +606,16 @@ by the dynamics alone; `src/stored_host_run.c` runs three generations of
587
  cells does the same with a description code of its own, every generation of it
588
  proved in Rocq. Its robustness section gives the margin every orbit has
589
  against perturbation, a certificate of the leakage an orbit tolerates from the
590
- inputs active along it, and the amplified host of `src/redundant.py`: every row
591
  of the levelized host replaced by r rows reading all r copies of its inputs,
592
  which multiplies every margin by r, reproduces itself exactly, fails only when two
593
  wrong copies meet, with a probability of second order that the paper bounds, and
594
  completes the self-reproducing run under read noise at levels where the host
595
  fails within its first bytes.
596
 
597
- The scripts that produce every figure are `src/selfrep.py`, `src/netlist_io.py`, `src/host_family.py`,
598
- `src/paper_stats.py`, `src/paper_verify.py`, `src/paper_runs.py`,
599
- `src/twohead.py`, `src/twohead_net.py`, `src/stored_host.py`,
600
- `src/stored_host_run.c`, `src/check_c.py`, `src/throughput.py`, `src/noise.py`,
601
- `src/redundant.py`, `src/neuromorphic.py` and the
602
- `src/check_*.py` set, and their records are in `paper/runs/`; `rocq/` holds the
603
- Rocq development, which `src/check_rocq.py` builds.
604
 
605
  ---
606
 
@@ -707,11 +722,11 @@ the Heaviside AND/XOR weights that realize each gate, so the file holds both the
707
  reversible program and the threshold substrate it runs on.
708
 
709
  ```bash
710
- python src/reversible.py # reversible gates, Cuccaro ALU, Bennett construction
711
- python src/reversible_cpu.py # bijective transition and backward execution
712
- python src/reversible_prog.py # structured reversible programs (multiply, Fibonacci, Janus IF)
713
- python tools/build_reversible.py # ship + round-trip variants/neural_reversible.safetensors
714
- python tools/reversible_matrix.py # ternary matrix stack: permutation transition + 0.5 margin
715
  ```
716
 
717
  ---
@@ -747,8 +762,8 @@ circuits. With mirror routing and a constant-particle source the construction is
747
  functionally complete (Margolus 1984).
748
 
749
  ```bash
750
- python src/ca.py # rule bijection, lattice reversibility, ballistic motion, interaction gate
751
- python tools/build_ca.py # ship the block rule as a ternary matrix tile (permutation + 0.5 margin)
752
  ```
753
 
754
  ---
@@ -780,8 +795,8 @@ bias (`-tau`); reloading it regrows the counter and reproduces the binding
780
  decision from the gate.
781
 
782
  ```bash
783
- python src/tile.py # binding gate, XOR/AND/OR rule tiles, binary counter
784
- python tools/build_tile.py # ship + regrow variants/neural_tile.safetensors
785
  ```
786
 
787
  ---
@@ -947,9 +962,9 @@ Alternative architectures: `PositionExtractor` (~1.5 M, position-specific, no at
947
  ### Training
948
 
949
  ```bash
950
- python train.py --mode router --epochs 100 # sanity check
951
- python train.py --mode llm --epochs 100 --batch_size 256 # frozen LLM
952
- python train.py --mode llm --unfreeze_layers 4 --batch_size 4096 # fine-tune top layers
953
  ```
954
 
955
  Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight), and CE on operation classification. Curriculum runs 0–9 → 0–99 → 0–255. Optimizer is AdamW, lr 3e-4, gradient clipping 1.0.
@@ -966,89 +981,42 @@ variants/ 18 fitness variants + 9 standalone machines
966
  neural_subleq8io, neural_reflect,
967
  neural_attractor, neural_reversible, neural_ca,
968
  neural_tile)
969
- src/ the library (run scripts as `python src/<name>.py`)
970
- ├── build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
971
- ├── quantize.py min integer dtypes + ternary verification/repair
972
- ├── eval.py gate-level fitness suite, NetlistEvaluator, float oracles, reference CPU
973
- ├── eval_all.py variant-agnostic harness + manifest-sized threshold CPU
974
- ├── machines.py neural_subleq8 + neural_rv32 runtimes, references, assemblers,
975
- │ RV32 object loader, and the test suites (subleq / rv32 / rv32-c)
976
- ├── matrix8.py compiles the CPU to a recurrent ternary matrix stack; the
977
- │ exhaustive equality suite and the analog crossbar simulation
978
- ├── constructor8.py the neural_subleq8io host, the recipe codec, and the universal
979
- │ constructor / self-reproduction suite
980
- ├── reflect.py neural_reflect: a universal threshold interpreter whose state
981
- │ holds its own gate table; universality / quine / matrix suites
982
- ├── attractor.py neural_attractor: energy-based solver (forward eval, inversion,
983
- │ SAT) built from a QUBO gate-gadget compiler
984
- ├── reversible.py neural_reversible: reversible threshold gates, Cuccaro ALU,
985
- │ Bennett construction
986
- ├── reversible_cpu.py reversible register machine: bijective step, backward execution
987
- ├── reversible_prog.py structured reversible programs (multiply, Fibonacci, Janus IF)
988
- ├── ca.py neural_ca: reversible Margolus cellular automaton, threshold
989
- │ block rule, lattice reversibility and billiard-ball dynamics
990
- ├── tile.py neural_tile: abstract tile assembly, threshold binding rule,
991
- │ XOR/Sierpinski and binary-counter tile sets
992
- ├── selfrep.py the tape device, the constructor P and the self-reproducing
993
- │ program P*, the framing maps, and three evaluators
994
- ├── canonical.py deterministic safetensors writer (pinned key and tensor order)
995
- ├── paper_stats.py unit counts, levelization sizes, effective fan-in
996
- ├── paper_verify.py codec, framing, serialization, datapath, levelization
997
- ├── paper_runs.py self-description, three generations, lockstep
998
- ├── host_family.py the host family H_A: a constant-depth SUBLEQ step for
999
- │ every word width, and its serialization in build order
1000
- ├── netlist_eval.c an evaluator of the host family, the amplified host and the
1001
- │ interpreter in C, the levels of a large netlist in parallel
1002
- ├── check_c.py the host on the evaluator in C
1003
- ├── twohead.py the two-head machine, transcribed, the description code
1004
- │ and the translating organism; writes rocq/Organism.v
1005
- ├── twohead_net.py the two-head machine as a ternary netlist N_{G,L} for a window
1006
- │ of 2^α cells: its check, its file, the organism on it in C,
1007
- │ and the comparison with the netlist extracted from rocq/
1008
  ├── twohead_lockstep.c the organism on N_{G,L} beside the transcription, in C
1009
- ├── stored_host.py the host compiled into 36,074 records of the two-head machine
1010
- │ with 4096 cells: one pass against the host's step and device,
1011
- │ the image and its recipe, and its generations
1012
  ├── stored_host_run.c the stored host's generations on the two-head machine, in C
1013
  ├── restore_bound.c the restoration bound of the amplified host along the orbit
1014
- ├── check_rocq.py builds rocq/ and prints the assumptions of its theorems
1015
- ├── independent/ separate code written from the paper's definitions: the host
1016
- │ family, the levelization, a noise simulator in C, weight
1017
- │ error, and the neuromorphic datapath
1018
- ├── redundant.py the amplified host N_r: r-fold redundancy with the restoring
1019
- │ organ inside every unit, its self-reproduction, its runs
1020
- │ under read noise
1021
- ├── noise.py read noise on the self-reproducing run, the deviation curve
1022
- │ against the bounds, weight error with zero-entry leakage, the
1023
- │ active-input counts that certify an orbit
1024
- ├── neuromorphic.py the datapath on the Loihi and Akida neuron models
1025
- └── check_*.py codec boundaries and optimality, step counts, ternary depth,
1026
- environment cost, device, host, host family, levelization,
1027
- framing, serialization, all 29 machines
1028
  paper/ the manuscript, and the record of every run it reports
1029
  rocq/ Rocq proofs: the netlist of the two-head machine computes its
1030
  step for every window of 2^α cells, the organism builds every
1031
  image from any description of it and reproduces in every
1032
  generation
1033
- tools/ build_all.py (build + quantize + verify every profile),
1034
- cpu_programs.py (assembler + CPU program suite), test_cpu.py
1035
- (program suite vs a variant), play.py (interactive demo),
1036
- prune_weights.py (GPU-batched weight reduction),
1037
- build_attractor.py / test_attractor.py (neural_attractor),
1038
- build_reversible.py / reversible_matrix.py (neural_reversible),
1039
- build_ca.py (neural_ca matrix tile), build_tile.py (neural_tile)
1040
- llm_integration/ SmolLM2 extractor + circuit wrapper + training code
1041
- ├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
1042
- │ add_8bit / sub_8bit / mul_8bit / compare_*)
1043
- ├── model.py Extractor variants, ArithmeticModel
1044
- ├── train.py router / interface / llm training modes
1045
- ├── fitness.py randomized fitness function
1046
- ├── baseline.py vanilla SmolLM2 baseline measurement
1047
  ├── trained/ checkpointed extractor weights
1048
- └── smollm2/
1049
- ├── SMOLLM2_ARCHITECTURE.md architecture analysis
1050
- ├── analyze_smollm2.py analysis script
1051
- └── smollm2_analysis.json analysis output
1052
  ```
1053
 
1054
  ---
 
99
  Run the full circuit verification suite against any variant:
100
 
101
  ```bash
102
+ python src/eval.py variants variants/ # 18 fitness variants (machines skipped)
103
+ python src/eval.py variants neural_computer.safetensors # the canonical file
104
+ python src/eval.py variants --cpu-program variants/ # also run an assembled
105
  # program through the
106
  # threshold-gated CPU
107
  ```
108
 
109
+ `src/eval.py variants` reads each variant's manifest, runs a gate-level fitness suite (13,900–15,900 tests per variant covering Boolean, arithmetic, ALU, control, modular, error-detection, threshold, and float circuits, including end-to-end evaluation of the composed float pipelines from the shipped wiring metadata — see the Verification table), and optionally executes a small assembled program through a manifest-sized threshold CPU plus a chained 16- or 32-bit ALU sequence on wider variants.
110
 
111
  For an interactive walkthrough that exercises Boolean gates, the 8-bit ALU, mod-5 divisibility, and a CPU loop end-to-end:
112
 
 
115
  python tools/play.py --model neural_computer.safetensors # 64 KB, slower
116
  ```
117
 
118
+ Each machine of the family also has a demonstration on a named task, which loads the shipped weights and checks its result against an independent reference:
119
+
120
+ | Command | Machine | What it does |
121
+ |---------|---------|--------------|
122
+ | `python tools/play.py sieve` | `neural_computer8` | Sieve of Eratosthenes; with no indexed addressing, the program rewrites the address bytes of its own LOAD/STORE. Finds all 54 primes < 256. |
123
+ | `python tools/play.py gcd` | `neural_computer8` | Euclid's GCD, with `a mod b` built from the DIV/MUL/SUB opcodes and an XOR register swap. |
124
+ | `python tools/play.py collatz` | `neural_computer8` | Collatz step counts; exact for every seed whose trajectory stays inside the 8-bit range. |
125
+ | `python tools/play.py pi` | `neural_rv32` | pi to nine digits by Machin's 1706 arctangent formula, printed to the console. |
126
+ | `python tools/play.py nets` | `neural_rv32` | Ternary neural nets run as NEUR software: one learned by gradient descent, one (XOR) compiled by construction. |
127
+ | `python tools/play.py fleet` | `neural_matrix8` | 65,536 CPUs stepped in lockstep on the GPU as one batched matrix product. |
128
+ | `python tools/play.py factoring` | `neural_attractor` | Factors semiprimes by relaxing an 8x8 multiplier's energy backward; also divides. |
129
+ | `python tools/play.py nqueens` | `neural_attractor` | Solves 6- and 8-queens by relaxing toward zero unsatisfied clauses. |
130
+ | `python tools/play.py constructor` | `neural_subleq8io` | The universal constructor fabricates a sibling machine byte-for-byte, which then boots. |
131
+ | `python tools/play.py stack` | `neural_reflect` | Self-modifying SUBLEQ code, on a stored machine, on the fixed interpreter: three levels in one tensor. |
132
+ | `python tools/play.py counterfactual` | `neural_reversible` | Bijective mixing, exact reverse recovery, and the counterfactual when a digest bit is flipped. |
133
+ | `python tools/play.py loschmidt` | `neural_ca` | A ~2,000-particle gas mixed 500 steps and un-mixed exactly; one flipped cell corrupts half the past. |
134
+ | `python tools/play.py cipher` | `neural_ca` | The reversible automaton as a block cipher: exact decryption with the right key, noise with a wrong one. |
135
+ | `python tools/play.py pascal` | `neural_tile` | Self-assembly grows Pascal's triangle mod 2; every cell verified against Lucas' theorem. |
136
+
137
  For end-to-end CPU validation (Fibonacci, sum 1..N, bubble sort, self-modifying JMP, all eight conditional jumps, CALL stack semantics, MUL cross-checked against repeated ADD, DIV cross-checked against repeated SUB, a bitwise AND/OR/XOR/SHL/SHR chain, and the architectural flag policy):
138
 
139
  ```bash
140
+ python src/eval.py cpu # default: 1 KB, ~2 s
141
+ python src/eval.py cpu --model neural_computer.safetensors # 64 KB canonical, ~2 min
142
+ python src/eval.py cpu --only fib,sum_n # subset of suite
143
  ```
144
 
145
+ Each program is assembled by a small Python assembler (in `src/eval.py`) and run through the threshold-gated CPU; the driver verifies expected memory contents at HALT.
146
 
147
  ---
148
 
 
344
  To regenerate every named variant in one pass:
345
 
346
  ```bash
347
+ python src/build.py variants
348
  ```
349
 
350
  This populates `variants/` with all 18 builds, quantizes each one to the smallest signed integer dtype that exactly represents its weights (~4× reduction in tensor data, with file size dominated by the safetensors header on the smaller profiles), verifies the strictly ternary weight invariant (`--ternary --strict`, so a build with any non-ternary weight fails loudly), stamps the `weight_quantization` metadata field, and runs `src/eval.py` on each as a sanity check.
 
352
  The quantizer is also available standalone:
353
 
354
  ```bash
355
+ python src/build.py quantize path/to/file.safetensors # in-place
356
+ python src/build.py quantize variants/ # whole directory
357
+ python src/build.py quantize model.safetensors -o quantized.safetensors
358
+ python src/build.py quantize file.safetensors --ternary # push toward {-1, 0, 1} weights
359
+ python src/build.py quantize file.safetensors --ternary --strict # error if any weight is non-ternary
360
  ```
361
 
362
  Every weight and bias tensor in the canonical model fits in `int8`. The eval pipeline promotes weights to `float32` on load, so integer storage is exact and transparent.
 
388
 
389
  The 8-bit arithmetic and ALU tests sample strategically; the circuits are constructed gate by gate, so the full 65,536-case sweep adds nothing. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
390
 
391
+ `src/eval.py variants` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing evaluates the shipped weights; rebuilding is a separate step.** `python src/eval.py variants variants/` scores all 18 fitness variants against the shipped weights in about two minutes (~6 s each, the composed float netlists evaluated in `NetlistEvaluator`'s leveled mode) and cleanly skips the nine standalone machines. Rebuilding the models (`python src/build.py variants`, ~50 min for all 18) is a separate step, needed only when the circuit constructions in `src/build.py` change; routine verification never rebuilds. The batched evaluator is population-safe: every chained intermediate (carry, borrow, mux select) is computed per population slot, with no slot-0 approximation anywhere.
392
 
393
  ---
394
 
 
424
 
425
  ```bash
426
  python src/build.py --apply rv32 # build the file
427
+ python src/build.py quantize variants/neural_rv32.safetensors --ternary --strict
428
  python src/machines.py rv32 # eight-program lockstep suite
429
  python src/machines.py rv32-c # stock-compiler C, end to end
430
  ```
 
480
  conductance mismatch (bit-exact through σ_G = 0.10).
481
 
482
  ```bash
483
+ python src/machines.py matrix8 build # compile + save variants/neural_matrix8.safetensors
484
+ python src/machines.py matrix8 verify # exhaustive equality vs the gate graph and integer reference
485
+ python src/machines.py matrix8 analog # margin measurement + read-noise + conductance-mismatch sweeps
486
  ```
487
 
488
  Because the transition is a fixed matrix stack proven equal to the gate-graph
 
527
  within a fixed annealing schedule.
528
 
529
  ```bash
530
+ python src/automata.py build-attractor # compile a multiplier to variants/neural_attractor.safetensors
531
+ python src/automata.py attractor # forward eval, whole-network relaxation, factoring, SAT
532
  ```
533
 
534
  ---
 
568
  closing the loop across two generations.
569
 
570
  ```bash
571
+ python src/host.py constructor8 build # emit variants/neural_subleq8io.safetensors
572
+ python src/host.py constructor8 verify # host soundness + codec round-trip + constructor on all 3 backends
573
+ python src/host.py constructor8 self # full self-reproduction on the threshold matrices, then gen-2 boot
574
  ```
575
 
576
  A program running on the processor emits the exact tensor file that defines
 
581
  `paper/` states and proves what this machine does, for a host serialized one
582
  unit at a time: `variants/neural_subleq8io_netlist.safetensors` stores the bias
583
  of every unit and every edge of the wiring, 17,598 units and 51,132 edges in
584
+ 160,240 bytes, in the order `src/host.py` builds them, so the network the
585
  file denotes is the network the paper analyses, and parsing the file rebuilds
586
  it. That host is the member with eight-bit words of a family whose step has
587
  depth 13 for every word width, with a carry-lookahead subtractor and the
588
+ operands fetched through the decode lines of the counter; `src/host.py`
589
+ builds any member and `python src/paper.py host-family` checks each against SUBLEQ
590
  on the evaluator in C. Separating construction from copying
591
  as von Neumann did, a 27-instruction program emits the serialization of the
592
  host, its initial memory and its own description tape; each generation is
 
594
  rebuilding the netlist from its first field, and building the evaluator from
595
  that netlist. It also shows where the restriction to {-1, 0, 1} is paid for,
596
  and that a fixed ternary network, the two-head machine of
597
+ `src/twohead.py`, holds a stored network in its state and evaluates one
598
  record per step, which `rocq/` proves for every window of 2^α cells, bank and
599
+ tape. The host itself, compiled by `src/twohead.py` into 36,074 records of
600
  two inputs with records that load its memory, its tape device and a hand-over,
601
  and stored in the member with 4096 cells, runs the 27-instruction program on a
602
  recipe for its own records: it writes them into the idle bank, copies the recipe
 
606
  cells does the same with a description code of its own, every generation of it
607
  proved in Rocq. Its robustness section gives the margin every orbit has
608
  against perturbation, a certificate of the leakage an orbit tolerates from the
609
+ inputs active along it, and the amplified host of `src/noise.py`: every row
610
  of the levelized host replaced by r rows reading all r copies of its inputs,
611
  which multiplies every margin by r, reproduces itself exactly, fails only when two
612
  wrong copies meet, with a probability of second order that the paper bounds, and
613
  completes the self-reproducing run under read noise at levels where the host
614
  fails within its first bytes.
615
 
616
+ Every figure of the paper comes from `src/paper.py`, `src/host.py`, `src/twohead.py` and
617
+ `src/noise.py`, with the C programs beside them, and their records are in `paper/runs/`;
618
+ `rocq/` holds the Rocq development, which `python src/paper.py rocq` builds.
 
 
 
 
619
 
620
  ---
621
 
 
722
  reversible program and the threshold substrate it runs on.
723
 
724
  ```bash
725
+ python src/automata.py reversible # reversible gates, Cuccaro ALU, Bennett construction
726
+ python src/automata.py reversible-cpu # bijective transition and backward execution
727
+ python src/automata.py reversible-prog # structured reversible programs (multiply, Fibonacci, Janus IF)
728
+ python src/automata.py build-reversible # ship + round-trip variants/neural_reversible.safetensors
729
+ python src/automata.py reversible-matrix# ternary matrix stack: permutation transition + 0.5 margin
730
  ```
731
 
732
  ---
 
762
  functionally complete (Margolus 1984).
763
 
764
  ```bash
765
+ python src/automata.py ca # rule bijection, lattice reversibility, ballistic motion, interaction gate
766
+ python src/automata.py build-ca # ship the block rule as a ternary matrix tile (permutation + 0.5 margin)
767
  ```
768
 
769
  ---
 
795
  decision from the gate.
796
 
797
  ```bash
798
+ python src/automata.py tile # binding gate, XOR/AND/OR rule tiles, binary counter
799
+ python src/automata.py build-tile # ship + regrow variants/neural_tile.safetensors
800
  ```
801
 
802
  ---
 
962
  ### Training
963
 
964
  ```bash
965
+ python llm_integration/llm.py train --mode router --epochs 100 # the router alone
966
+ python llm_integration/llm.py train --mode llm --epochs 100 --batch_size 256 # frozen LLM
967
+ python llm_integration/llm.py train --mode llm --unfreeze_layers 4 --batch_size 4096 # fine-tune top layers
968
  ```
969
 
970
  Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight), and CE on operation classification. Curriculum runs 0–9 → 0–99 → 0–255. Optimizer is AdamW, lr 3e-4, gradient clipping 1.0.
 
981
  neural_subleq8io, neural_reflect,
982
  neural_attractor, neural_reversible, neural_ca,
983
  neural_tile)
984
+ src/ the library; `python src/<file>.py <command>`
985
+ ├── build.py the generator (also `subleq`, `rv32`), `quantize`, and
986
+ │ `variants`, which builds, quantizes and scores every variant
987
+ ├── eval.py the gate-level fitness suite, NetlistEvaluator, float oracles;
988
+ │ `variants` scores every variant, `cpu` runs the CPU programs
989
+ ├── machines.py the SUBLEQ, RV32 and matrix machines: runtimes, references,
990
+ │ assemblers and suites (`subleq`, `rv32`, `rv32-c`, `matrix8`)
991
+ ├── reflect.py neural_reflect: a threshold interpreter whose state holds its
992
+ │ own gate table; universality, quine and matrix suites
993
+ ├── automata.py the attractor, reversible, cellular-automaton and tile machines,
994
+ │ their suites and their builders
995
+ ├── host.py the SUBLEQ host: its family, serialization, device, the
996
+ │ constructor and the self-reproducing program, the evaluators,
997
+ │ and `constructor8`, the neural_subleq8io suite
998
+ ├── twohead.py the two-head machine: the organism (`organism`), the netlist
999
+ │ N_{G,L} (`net`), and the host stored in it (`stored`)
1000
+ ├── noise.py read noise and weight error on the host (`host`), the amplified
1001
+ │ host (`amplified`), and the neuromorphic datapath
1002
+ ├── paper.py the paper's checks and runs, one command each
1003
+ ├── independent.py code written from the paper's text alone
1004
+ ├── netlist_eval.c an evaluator of the host family and the amplified host in C
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1005
  ├── twohead_lockstep.c the organism on N_{G,L} beside the transcription, in C
 
 
 
1006
  ├── stored_host_run.c the stored host's generations on the two-head machine, in C
1007
  ├── restore_bound.c the restoration bound of the amplified host along the orbit
1008
+ └── nsim.c the noise simulator of independent.py
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  paper/ the manuscript, and the record of every run it reports
1010
  rocq/ Rocq proofs: the netlist of the two-head machine computes its
1011
  step for every window of 2^α cells, the organism builds every
1012
  image from any description of it and reproduces in every
1013
  generation
1014
+ tools/play.py the playground, and a demonstration on each machine
1015
+ llm_integration/
1016
+ ├── llm.py SmolLM2 with the frozen circuits: the circuit wrapper, the
1017
+ │ fitness, the extractors, training and the baseline
 
 
 
 
 
 
 
 
 
 
1018
  ├── trained/ checkpointed extractor weights
1019
+ └── smollm2/ the architecture of SmolLM2 and its analysis
 
 
 
1020
  ```
1021
 
1022
  ---
demos/README.md DELETED
@@ -1,22 +0,0 @@
1
- # demos
2
-
3
- Standalone programs that put each machine in the family to work on a named
4
- task. Every script is self-contained (`python demos/<name>.py`), loads the
5
- shipped weights, and cross-checks its result against an independent reference.
6
-
7
- | Demo | Machine | What it does |
8
- |------|---------|--------------|
9
- | `neural_computer8_self_modifying_sieve.py` | `neural_computer8` | Sieve of Eratosthenes; with no indexed addressing, the program rewrites the address bytes of its own LOAD/STORE. Finds all 54 primes < 256. |
10
- | `neural_computer8_euclid_gcd.py` | `neural_computer8` | Euclid's GCD, with `a mod b` built from the DIV/MUL/SUB opcodes and an XOR register swap. |
11
- | `neural_computer8_collatz.py` | `neural_computer8` | Collatz step counts; exact for every seed whose trajectory stays inside the 8-bit range. |
12
- | `neural_rv32_machin_pi.py` | `neural_rv32` | pi to nine digits by Machin's 1706 arctangent formula, printed to the console. |
13
- | `neural_rv32_neural_nets_via_neur.py` | `neural_rv32` | Ternary neural nets run as NEUR software: one learned by gradient descent, one (XOR) compiled by construction. |
14
- | `neural_matrix8_gpu_cpu_fleet.py` | `neural_matrix8` | 65,536 CPUs stepped in lockstep on the GPU as one batched matrix product. |
15
- | `neural_attractor_factoring.py` | `neural_attractor` | Factors semiprimes by relaxing an 8x8 multiplier's energy backward; also divides. |
16
- | `neural_attractor_nqueens.py` | `neural_attractor` | Solves 6- and 8-queens by relaxing toward zero unsatisfied clauses. |
17
- | `neural_subleq8io_universal_constructor.py` | `neural_subleq8io` | The universal constructor fabricates a sibling machine byte-for-byte, which then boots. |
18
- | `neural_reflect_self_modifying_stack.py` | `neural_reflect` | Self-modifying SUBLEQ code, on a stored machine, on the fixed interpreter: three levels in one tensor. |
19
- | `neural_reversible_counterfactual.py` | `neural_reversible` | Bijective mixing, exact reverse recovery, and the counterfactual when a digest bit is flipped. |
20
- | `neural_ca_loschmidt_echo.py` | `neural_ca` | A ~2,000-particle gas mixed 500 steps and un-mixed exactly; one flipped cell corrupts half the past. |
21
- | `neural_ca_reversible_cipher.py` | `neural_ca` | The reversible automaton as a block cipher: exact decryption with the right key, noise with a wrong one. |
22
- | `neural_tile_pascal_lucas.py` | `neural_tile` | Self-assembly grows Pascal's triangle mod 2; every cell verified against Lucas' theorem. |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_attractor_factoring.py DELETED
@@ -1,55 +0,0 @@
1
- """neural_attractor -- factoring semiprimes by running a multiplier backward.
2
-
3
- An 8x8 array multiplier compiles to a quadratic energy that is 0 exactly on its
4
- truth table. Clamp the 16 product bits and relax the 16 input bits and the
5
- network settles into a factorization: multiplication run in reverse. Clamp one
6
- input as well and the same circuit performs division.
7
-
8
- python demos/neural_attractor_factoring.py
9
- """
10
- import os, sys, time
11
- HERE = os.path.dirname(os.path.abspath(__file__))
12
- REPO = os.path.dirname(HERE)
13
- sys.path.insert(0, os.path.join(REPO, "src"))
14
- from attractor import multiplier
15
-
16
-
17
- if __name__ == "__main__":
18
- c, io = multiplier(8)
19
- print("neural_attractor: factoring by energy relaxation")
20
- print("=" * 56)
21
- print(f"8x8 array multiplier as an energy landscape: {len(c.gates)} gate "
22
- f"gadgets, {c.n} wires, 16 free input bits\n")
23
-
24
- ladder = [(3599, 59, 61), (9797, 97, 101), (32399, 179, 181), (57599, 239, 241)]
25
- for N, pa, pb in ladder:
26
- target = {io["prod"][k]: (N >> k) & 1 for k in range(16)}
27
- t0, got = time.perf_counter(), None
28
- for attempt in range(1, 8):
29
- s = c.solve(io["xs"] + io["ys"], {io["zero"]: 0}, target,
30
- sweeps=2500, restarts=12, seed=N * 7 + attempt)
31
- if s is not None:
32
- x = sum(s[io["xs"][k]] << k for k in range(8))
33
- y = sum(s[io["ys"][k]] << k for k in range(8))
34
- if x * y == N and x > 1 and y > 1:
35
- got = (x, y, attempt); break
36
- dt = time.perf_counter() - t0
37
- if got:
38
- x, y, att = got
39
- tag = "the prime pair" if {x, y} == {pa, pb} else "valid factorization"
40
- print(f" {N:5d} = {x:3d} x {y:3d} ({dt:4.1f}s, attempt {att}) [{tag}]")
41
- else:
42
- print(f" {N:5d}: ground state not reached in budget ({dt:.1f}s)")
43
-
44
- # division: clamp one input and the product, relax the other input
45
- N, xa = 57599, 241
46
- fixed = {io["zero"]: 0}
47
- for k in range(8):
48
- fixed[io["xs"][k]] = (xa >> k) & 1
49
- target = {io["prod"][k]: (N >> k) & 1 for k in range(16)}
50
- t0 = time.perf_counter()
51
- s = c.solve(io["ys"], fixed, target, sweeps=800, restarts=10, seed=7)
52
- if s is not None:
53
- y = sum(s[io["ys"][k]] << k for k in range(8))
54
- print(f"\n division mode: clamp x=241 and the product -> y = {y} "
55
- f"({time.perf_counter()-t0:.1f}s; {xa} x {y} = {xa * y})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_attractor_nqueens.py DELETED
@@ -1,90 +0,0 @@
1
- """neural_attractor -- the N-queens problem solved by energy relaxation.
2
-
3
- The attractor computer has no clock and no program counter: a circuit compiles
4
- to a quadratic energy whose global minimum is its consistent assignment, and
5
- "running" it is rolling downhill. Here every N-queens constraint (one queen per
6
- row, no two sharing a column or diagonal) becomes a clause wire that must be 1;
7
- the annealer relaxes the board variables toward zero unsatisfied clauses. This
8
- is the min-conflicts / MAX-SAT objective, expressed entirely in the family's
9
- Heaviside threshold gadgets.
10
-
11
- python demos/neural_attractor_nqueens.py
12
- """
13
- import os, sys, time
14
- HERE = os.path.dirname(os.path.abspath(__file__))
15
- REPO = os.path.dirname(HERE)
16
- sys.path.insert(0, os.path.join(REPO, "src"))
17
- from attractor import Circuit
18
-
19
-
20
- def queens_circuit(N):
21
- """Build the N-queens CNF as a threshold circuit; return every clause wire
22
- so the solver can drive them all to 1 (a graded, per-clause objective)."""
23
- c = Circuit()
24
- v = {(r, col): c.wire() for r in range(N) for col in range(N)}
25
- clauses = []
26
-
27
- def OR_lits(lits):
28
- acc = lits[0]
29
- for w in lits[1:]:
30
- acc = c.OR(acc, w)
31
- return acc
32
-
33
- for r in range(N): # each row: >= 1 queen
34
- clauses.append(OR_lits([v[(r, col)] for col in range(N)]))
35
- for c1 in range(N): # each row: <= 1 queen
36
- for c2 in range(c1 + 1, N):
37
- clauses.append(OR_lits([c.NOT(v[(r, c1)]), c.NOT(v[(r, c2)])]))
38
- for col in range(N): # each column: <= 1
39
- for r1 in range(N):
40
- for r2 in range(r1 + 1, N):
41
- clauses.append(OR_lits([c.NOT(v[(r1, col)]), c.NOT(v[(r2, col)])]))
42
- cells = [(r, col) for r in range(N) for col in range(N)]
43
- for i in range(len(cells)): # each diagonal: <= 1
44
- for j in range(i + 1, len(cells)):
45
- (r1, c1), (r2, c2) = cells[i], cells[j]
46
- if r1 - c1 == r2 - c2 or r1 + c1 == r2 + c2:
47
- clauses.append(OR_lits([c.NOT(v[(r1, c1)]), c.NOT(v[(r2, c2)])]))
48
- return c, v, clauses
49
-
50
-
51
- def valid(board, N):
52
- qs = [rc for rc, b in board.items() if b]
53
- if len(qs) != N:
54
- return False
55
- for i in range(len(qs)):
56
- for j in range(i + 1, len(qs)):
57
- (r1, c1), (r2, c2) = qs[i], qs[j]
58
- if r1 == r2 or c1 == c2 or abs(r1 - r2) == abs(c1 - c2):
59
- return False
60
- return True
61
-
62
-
63
- def solve_board(N, seeds=8):
64
- c, v, clauses = queens_circuit(N)
65
- free = list(v.values())
66
- target = {w: 1 for w in clauses}
67
- t0 = time.perf_counter()
68
- for seed in range(seeds):
69
- s = c.solve(free, {}, target, sweeps=6000, restarts=6, seed=seed)
70
- if s is not None:
71
- board = {rc: s[w] for rc, w in v.items()}
72
- if valid(board, N):
73
- return board, len(clauses), c.n, time.perf_counter() - t0
74
- return None, len(clauses), c.n, time.perf_counter() - t0
75
-
76
-
77
- if __name__ == "__main__":
78
- print("neural_attractor: N-queens as energy relaxation")
79
- print("=" * 56)
80
- for N in (6, 8):
81
- board, nclauses, nwires, dt = solve_board(N)
82
- status = "SOLVED" if board else "no solution reached in budget"
83
- print(f"\n{N}-queens: {nclauses} clause gadgets over {nwires} wires -> "
84
- f"{status} ({dt:.1f}s)")
85
- if board:
86
- for r in range(N):
87
- print(" " + " ".join("Q" if board[(r, col)] else "." for col in range(N)))
88
- print(f" verified: {N} queens, no shared row/column/diagonal")
89
- print("\nNo search loop was written: the constraints are an energy surface and")
90
- print("the solution is its floor. The queens fall into place.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_ca_loschmidt_echo.py DELETED
@@ -1,55 +0,0 @@
1
- """neural_ca -- a Loschmidt echo in a reversible cellular automaton.
2
-
3
- The Margolus block rule is a bijection, so a gas of ~2,000 particles mixed for
4
- 500 steps can be un-mixed by iterating the same rule backward: the initial
5
- configuration returns cell-for-cell (particle number conserved throughout). Yet
6
- flipping a single cell of the mixed state before reversing corrupts roughly half
7
- the reconstructed past -- exact reversibility and sensitive dependence in the
8
- same automaton.
9
-
10
- python demos/neural_ca_loschmidt_echo.py
11
- """
12
- import os, sys, time, random, statistics
13
- HERE = os.path.dirname(os.path.abspath(__file__))
14
- REPO = os.path.dirname(HERE)
15
- sys.path.insert(0, os.path.join(REPO, "src"))
16
- import ca
17
-
18
-
19
- def coarse(g, H, W, k=8):
20
- out = []
21
- for by in range(0, H, k):
22
- for bx in range(0, W, k):
23
- out.append(sum(g[y][x] for y in range(by, by + k) for x in range(bx, bx + k)))
24
- return out
25
-
26
-
27
- if __name__ == "__main__":
28
- H = W = 64
29
- rng = random.Random(2026)
30
- grid = [[1 if rng.random() < 0.5 else 0 for _ in range(W)] for _ in range(H)]
31
- n0 = sum(map(sum, grid))
32
- STEPS = 500
33
-
34
- print("neural_ca: Loschmidt echo (mix, then run time backward)")
35
- print("=" * 56)
36
- t0 = time.perf_counter()
37
- fwd = ca.run(grid, STEPS, 0)
38
- n1 = sum(map(sum, fwd))
39
- back = ca.run_back(fwd, STEPS, 0)
40
- echo = back == grid
41
- dt = time.perf_counter() - t0
42
-
43
- print(f"particles: {n0} at t=0, {n1} at t={STEPS} "
44
- f"({'conserved' if n0 == n1 else 'NOT CONSERVED'})")
45
- print(f"coarse 8x8 occupancy stdev: t=0 {statistics.pstdev(coarse(grid, H, W)):.2f} "
46
- f"-> t={STEPS} {statistics.pstdev(coarse(fwd, H, W)):.2f} (mixed)")
47
- print(f"{STEPS} steps forward + {STEPS} reversed in {dt:.1f}s: "
48
- f"t=0 recovered {'EXACTLY' if echo else 'FAILED'}")
49
-
50
- flip = [row[:] for row in fwd]
51
- flip[0][0] ^= 1
52
- back2 = ca.run_back(flip, STEPS, 0)
53
- ham = sum(back2[y][x] != grid[y][x] for y in range(H) for x in range(W))
54
- print(f"butterfly: flip ONE cell at t={STEPS}, reverse again -> reconstructed "
55
- f"past wrong in {ham}/{H * W} cells")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_ca_reversible_cipher.py DELETED
@@ -1,84 +0,0 @@
1
- """neural_ca -- a reversible cellular automaton as a perfect cipher.
2
-
3
- The Margolus block rule is a bijection of the lattice, so iterating it forward
4
- diffuses a bitmap into noise and iterating the exact same rule backward restores
5
- it bit-for-bit. That makes the automaton a block cipher whose key is the pair
6
- (number of steps, starting partition phase): the right key inverts the diffusion
7
- perfectly, and a key off by a single step returns noise. No information is ever
8
- destroyed, so decryption is exact rather than approximate.
9
-
10
- python demos/neural_ca_reversible_cipher.py
11
- """
12
- import os, sys
13
- HERE = os.path.dirname(os.path.abspath(__file__))
14
- REPO = os.path.dirname(HERE)
15
- sys.path.insert(0, os.path.join(REPO, "src"))
16
- import ca
17
-
18
- # a recognizable plaintext bitmap (a heart), padded into a larger lattice
19
- ART = [
20
- " ### ### ",
21
- " ##### ##### ",
22
- "#############",
23
- "#############",
24
- "#############",
25
- " ########### ",
26
- " ######### ",
27
- " ####### ",
28
- " ##### ",
29
- " ### ",
30
- " # ",
31
- ]
32
- PAD_X, PAD_Y = 6, 4
33
-
34
-
35
- def make_grid():
36
- # the Margolus partition tiles 2x2 blocks toroidally, so H and W must be even
37
- h = len(ART) + 2 * PAD_Y
38
- w = len(ART[0]) + 2 * PAD_X
39
- h += h & 1
40
- w += w & 1
41
- g = [[0] * w for _ in range(h)]
42
- for y, row in enumerate(ART):
43
- for x, ch in enumerate(row):
44
- if ch == "#":
45
- g[y + PAD_Y][x + PAD_X] = 1
46
- return g
47
-
48
-
49
- def render(g):
50
- return "\n".join("".join("#" if c else "." for c in row) for row in g)
51
-
52
-
53
- def hamming(a, b):
54
- return sum(a[y][x] != b[y][x] for y in range(len(a)) for x in range(len(a[0])))
55
-
56
-
57
- if __name__ == "__main__":
58
- plain = make_grid()
59
- KEY_STEPS, KEY_PHASE = 200, 0
60
-
61
- cipher = ca.run(plain, KEY_STEPS, KEY_PHASE)
62
- recovered = ca.run_back(cipher, KEY_STEPS, KEY_PHASE)
63
- wrong = ca.run_back(cipher, KEY_STEPS - 1, KEY_PHASE) # key off by one step
64
-
65
- n = sum(map(sum, plain))
66
- print("neural_ca: reversible-automaton cipher")
67
- print("=" * 46)
68
- print(f"key = ({KEY_STEPS} steps, phase {KEY_PHASE}); {n} set bits (conserved "
69
- f"throughout: {sum(map(sum, cipher)) == n})\n")
70
-
71
- print("PLAINTEXT:")
72
- print(render(plain))
73
- print(f"\nCIPHERTEXT after {KEY_STEPS} forward steps (diffused to noise):")
74
- print(render(cipher))
75
- print(f"\nDECRYPTED with the correct key:")
76
- print(render(recovered))
77
- print(f" exact recovery: {recovered == plain} (Hamming distance 0)")
78
- print(f"\nDECRYPTED with a wrong key (off by one step):")
79
- print(render(wrong))
80
- print(f" still scrambled: {hamming(wrong, plain)} of {len(plain)*len(plain[0])} "
81
- f"cells differ from the plaintext")
82
- print("\nEvery step is a bijection, so the ciphertext holds exactly the")
83
- print("information of the plaintext -- no more, no less. Only the exact")
84
- print("reverse trajectory recovers it.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_computer8_collatz.py DELETED
@@ -1,76 +0,0 @@
1
- """neural_computer8 -- the Collatz map iterated on the threshold CPU.
2
-
3
- n -> n/2 if even, 3n+1 if odd, counting steps to 1. The parity test is a
4
- bitwise AND with 1 that leaves the flags untouched, so the loop branches on the
5
- comparison that precedes it. The register file is architecturally 8-bit, so the
6
- machine computes the Collatz trajectory modulo 256; for seeds whose true
7
- trajectory never exceeds 255, that is the exact Collatz step count, checked here
8
- against a native reference.
9
-
10
- python demos/neural_computer8_collatz.py
11
- """
12
- import os, sys, time
13
- HERE = os.path.dirname(os.path.abspath(__file__))
14
- REPO = os.path.dirname(HERE)
15
- sys.path.insert(0, os.path.join(REPO, "src"))
16
- sys.path.insert(0, os.path.join(REPO, "tools"))
17
- from safetensors import safe_open
18
- from cpu_programs import Asm
19
- from eval_all import GenericThresholdCPU, get_manifest
20
-
21
-
22
- def build(n0):
23
- a = Asm(1024)
24
- a.load(0, "N"); a.load(1, "ONE"); a.xor(2, 2) # R0=n, R1=1, R2=steps
25
- a.label("loop")
26
- a.cmp(0, 1); a.jz("done") # n == 1 ?
27
- a.xor(3, 3); a.add(3, 0); a.and_(3, 1) # R3 = n & 1 (flags preserved)
28
- a.cmp(3, 1); a.jz("odd")
29
- a.shr(0); a.jmp("step") # even: n >>= 1
30
- a.label("odd")
31
- a.xor(3, 3); a.add(3, 0); a.shl(3) # R3 = 2n
32
- a.add(0, 3); a.add(0, 1) # n = 3n + 1
33
- a.label("step")
34
- a.add(2, 1); a.jmp("loop") # steps++
35
- a.label("done")
36
- a.store(2, "RES"); a.halt()
37
- a.org(0x300); a.label("N"); a.db(n0)
38
- a.label("ONE"); a.db(1); a.label("RES"); a.db(0)
39
- return a, a.assemble()
40
-
41
-
42
- def true_collatz(n):
43
- peak, steps = n, 0
44
- while n != 1:
45
- n = n // 2 if n % 2 == 0 else 3 * n + 1
46
- peak = max(peak, n); steps += 1
47
- return steps, peak
48
-
49
-
50
- if __name__ == "__main__":
51
- tens = {}
52
- with safe_open(os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
53
- framework="pt") as f:
54
- for name in f.keys():
55
- tens[name] = f.get_tensor(name).float()
56
- cpu = GenericThresholdCPU(tens)
57
- addr = get_manifest(tens)["addr_bits"]
58
-
59
- print("neural_computer8: Collatz step counts through the gates")
60
- print("=" * 56)
61
- print(" seed CPU steps true steps peak in 8-bit range?")
62
- for n0 in (6, 7, 9, 18, 25, 45):
63
- a, mem = build(n0)
64
- state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": list(mem),
65
- "halted": False, "sp": (1 << addr) - 1}
66
- t0 = time.perf_counter()
67
- final, cycles = cpu.run(state, max_cycles=3000)
68
- steps = final["mem"][a.labels["RES"]]
69
- tsteps, peak = true_collatz(n0)
70
- inrange = peak <= 255
71
- match = "MATCH" if (inrange and steps == tsteps) else \
72
- ("mod-256" if not inrange else "MISMATCH")
73
- print(f" {n0:4d} {steps:9d} {tsteps:10d} {peak:4d} "
74
- f"{'yes' if inrange else 'no (wraps)':10s} [{match}]")
75
- print("\n For every seed whose trajectory stays under 256 the threshold CPU")
76
- print(" reproduces the exact Collatz step count.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_computer8_euclid_gcd.py DELETED
@@ -1,63 +0,0 @@
1
- """neural_computer8 -- Euclid's algorithm on the threshold CPU.
2
-
3
- The greatest common divisor by repeated remainder, where the remainder itself
4
- is synthesized from the machine's own opcodes: a mod b = a - (a / b) * b, using
5
- the DIV, MUL and SUB gates, and the register swap is three XORs (no temporary).
6
- Every step runs through threshold neurons.
7
-
8
- python demos/neural_computer8_euclid_gcd.py
9
- """
10
- import os, sys, time, math
11
- HERE = os.path.dirname(os.path.abspath(__file__))
12
- REPO = os.path.dirname(HERE)
13
- sys.path.insert(0, os.path.join(REPO, "src"))
14
- sys.path.insert(0, os.path.join(REPO, "tools"))
15
- from safetensors import safe_open
16
- from cpu_programs import Asm
17
- from eval_all import GenericThresholdCPU, get_manifest
18
-
19
-
20
- def build(a0, b0):
21
- g = Asm(1024)
22
- g.load(0, "A") # R0 = a
23
- g.load(1, "B") # R1 = b
24
- g.xor(3, 3) # R3 = 0
25
- g.label("loop")
26
- g.cmp(1, 3); g.jz("done") # while b != 0
27
- g.xor(2, 2); g.add(2, 0) # R2 = a
28
- g._alu(0x8, 2, 1) # R2 = a / b (DIV opcode)
29
- g.mul(2, 1) # R2 = (a/b)*b
30
- g.sub(0, 2) # R0 = a mod b
31
- g.xor(0, 1); g.xor(1, 0); g.xor(0, 1) # swap -> (b, a mod b)
32
- g.jmp("loop")
33
- g.label("done")
34
- g.store(0, "RES"); g.halt()
35
- g.org(0x300)
36
- g.label("A"); g.db(a0)
37
- g.label("B"); g.db(b0)
38
- g.label("RES"); g.db(0)
39
- return g, g.assemble()
40
-
41
-
42
- if __name__ == "__main__":
43
- tens = {}
44
- with safe_open(os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
45
- framework="pt") as f:
46
- for name in f.keys():
47
- tens[name] = f.get_tensor(name).float()
48
- cpu = GenericThresholdCPU(tens)
49
- addr = get_manifest(tens)["addr_bits"]
50
-
51
- print("neural_computer8: Euclid's GCD through the gates")
52
- print("=" * 56)
53
- for a0, b0 in [(84, 36), (120, 90), (221, 34), (255, 128)]:
54
- g, mem = build(a0, b0)
55
- state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": list(mem),
56
- "halted": False, "sp": (1 << addr) - 1}
57
- t0 = time.perf_counter()
58
- final, cycles = cpu.run(state, max_cycles=400)
59
- res = final["mem"][g.labels["RES"]]
60
- ok = res == math.gcd(a0, b0)
61
- print(f" gcd({a0:3d}, {b0:3d}) = {res:3d} ({cycles:3d} cycles, "
62
- f"{time.perf_counter()-t0:.1f}s) "
63
- f"[{'MATCH' if ok else 'MISMATCH vs ' + str(math.gcd(a0, b0))}]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_computer8_self_modifying_sieve.py DELETED
@@ -1,76 +0,0 @@
1
- """neural_computer8 -- the Sieve of Eratosthenes with self-modifying code.
2
-
3
- The threshold CPU's ISA has no indexed addressing, so the program does exactly
4
- what memory-constrained 1970s code did: it rewrites the address bytes of its own
5
- LOAD and STORE instructions before each access, walking a pointer across the
6
- flags array. Every gate that fetches, decodes, marks, and patches is a threshold
7
- neuron. It halts with all 54 primes below 256 marked.
8
-
9
- python demos/neural_computer8_self_modifying_sieve.py
10
- """
11
- import os, sys, time
12
- HERE = os.path.dirname(os.path.abspath(__file__))
13
- REPO = os.path.dirname(HERE)
14
- sys.path.insert(0, os.path.join(REPO, "src"))
15
- sys.path.insert(0, os.path.join(REPO, "tools"))
16
- from safetensors import safe_open
17
- from cpu_programs import Asm, _enc
18
- from eval_all import GenericThresholdCPU, get_manifest
19
-
20
- FLAGS = 0x300 # flags[0..255] at 0x300..0x3FF
21
-
22
-
23
- def build():
24
- g = Asm(1024)
25
- g.load(3, "ONE") # R3 = 1 (mark value / increment)
26
- g.load(0, "TWO") # R0 = p = 2
27
- g.label("outer")
28
- g.store(0, "Rlo") # patch probe address low byte <- p
29
- g.dw(_enc(0xA, 2, 0)) # probe: R2 = flags[p] (self-modified)
30
- g.db(FLAGS >> 8); g.label("Rlo"); g.db(0x00)
31
- g.cmp(2, 3); g.jz("next_p") # flags[p] == 1 -> composite, skip
32
- g.xor(1, 1); g.add(1, 0); g.add(1, 0) # m = 2p
33
- g.label("inner")
34
- g.store(1, "Wlo") # patch mark address low byte <- m
35
- g.dw(_enc(0xB, 0, 3)) # mark: flags[m] = R3 = 1 (self-modified)
36
- g.db(FLAGS >> 8); g.label("Wlo"); g.db(0x00)
37
- g.add(1, 0) # m += p (carry iff we passed 255)
38
- g.jnc("inner")
39
- g.label("next_p")
40
- g.add(0, 3) # p += 1
41
- g.load(2, "SIXTEEN")
42
- g.cmp(0, 2)
43
- g.jnz("outer") # loop until p == 16 (16^2 > 255)
44
- g.halt()
45
- g.org(0x200)
46
- g.label("ONE"); g.db(1)
47
- g.label("TWO"); g.db(2)
48
- g.label("SIXTEEN"); g.db(16)
49
- return g, g.assemble()
50
-
51
-
52
- if __name__ == "__main__":
53
- g, mem = build()
54
- tens = {}
55
- with safe_open(os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
56
- framework="pt") as f:
57
- for name in f.keys():
58
- tens[name] = f.get_tensor(name).float()
59
- cpu = GenericThresholdCPU(tens)
60
- state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": list(mem),
61
- "halted": False, "sp": (1 << get_manifest(tens)["addr_bits"]) - 1}
62
- t0 = time.perf_counter()
63
- final, cycles = cpu.run(state, max_cycles=4000)
64
- dt = time.perf_counter() - t0
65
-
66
- got = [n for n in range(2, 256) if final["mem"][FLAGS + n] == 0]
67
- ref = [n for n in range(2, 256) if all(n % d for d in range(2, int(n**0.5) + 1))]
68
- print("neural_computer8: self-modifying Sieve of Eratosthenes")
69
- print("=" * 56)
70
- print(f"halted={final['halted']} {cycles} cycles through the gates ({dt:.0f}s)")
71
- print(f"primes < 256 found: {len(got)} native sieve: {len(ref)} "
72
- f"{'EXACT MATCH' if got == ref else 'MISMATCH'}")
73
- print(" " + " ".join(map(str, got)))
74
- print(f"self-modified operand bytes at halt: probe={final['mem'][g.labels['Rlo']]}, "
75
- f"mark={final['mem'][g.labels['Wlo']]} (the program rewrote its own "
76
- f"instruction stream as it ran)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_matrix8_gpu_cpu_fleet.py DELETED
@@ -1,73 +0,0 @@
1
- """neural_matrix8 -- a fleet of processors as one batched matrix product.
2
-
3
- The whole CPU is compiled to 108 ternary weight matrices with a Heaviside step
4
- between them, so one clock cycle is one matrix-vector product plus a threshold.
5
- Add a batch dimension and N processors are the same 108 matmuls: here 65,536
6
- independent CPUs step in lockstep on the GPU. A 256-wide correctness fleet
7
- confirms every CPU halts exactly at cycle 2n+1 for its own countdown input.
8
-
9
- python demos/neural_matrix8_gpu_cpu_fleet.py
10
- """
11
- import os, sys, time
12
- HERE = os.path.dirname(os.path.abspath(__file__))
13
- REPO = os.path.dirname(HERE)
14
- sys.path.insert(0, os.path.join(REPO, "src"))
15
- import torch
16
- from matrix8 import MatrixMachine, state_to_vec, _mk_state, _instr
17
-
18
-
19
- def prog_bytes():
20
- return (_instr(0x1, 0, 1) # SUB R0, R1
21
- + _instr(0xD, 0, 0, 1) + [0, 0] # JNZ 0x0000 (address word)
22
- + _instr(0xF) # HALT
23
- + [0] * 8)
24
-
25
-
26
- if __name__ == "__main__":
27
- dev = "cuda" if torch.cuda.is_available() else "cpu"
28
- mm = MatrixMachine.from_file(device=dev)
29
- n_neurons = sum(int(b.numel()) for b in mm.B)
30
- name = torch.cuda.get_device_name(0) if dev == "cuda" else "cpu"
31
- print("neural_matrix8: 65,536 CPUs as one batched matrix product")
32
- print("=" * 56)
33
- print(f"loaded {len(mm.W)} ternary matrices, {n_neurons:,} threshold neurons "
34
- f"per transition, device={dev} ({name})")
35
- mem = prog_bytes()
36
-
37
- # correctness fleet: one CPU per 8-bit input, all 256 at once
38
- V = torch.stack([state_to_vec(_mk_state(mem=mem, regs=(x, 1, 0, 0)))
39
- for x in range(256)]).to(dev)
40
- first_halt = torch.full((256,), -1, dtype=torch.long)
41
- step = 0
42
- while step < 600:
43
- halted = V[:, MatrixMachine.HALT_IDX] > 0.5
44
- first_halt[(first_halt < 0) & halted.cpu()] = step
45
- if bool(halted.all()):
46
- break
47
- V = mm.step(V); step += 1
48
- expect = torch.tensor([2 * (x if x else 256) + 1 for x in range(256)])
49
- ok_halt = bool((first_halt == expect).all())
50
- ok_r0 = bool((V[:, 4:12].cpu().long().sum(1) == 0).all())
51
- print(f"\ncorrectness fleet of 256: every CPU halts at cycle 2n+1 for its own "
52
- f"input: {'EXACT' if ok_halt else 'MISMATCH'}; all results R0==0: {ok_r0}")
53
-
54
- # throughput fleet: 65,536 CPUs
55
- B = 65536
56
- V = torch.stack([state_to_vec(_mk_state(mem=mem, regs=(x & 0xFF, 1, 0, 0)))
57
- for x in range(256)]).repeat(256, 1).to(dev)
58
- if dev == "cuda":
59
- torch.cuda.synchronize()
60
- t0, steps = time.perf_counter(), 0
61
- while steps < 520:
62
- V = mm.step(V); steps += 1
63
- if steps % 64 == 0 and bool((V[:, MatrixMachine.HALT_IDX] > 0.5).all()):
64
- break
65
- if dev == "cuda":
66
- torch.cuda.synchronize()
67
- dt = time.perf_counter() - t0
68
- instr = B * steps
69
- print(f"\nthroughput fleet of {B:,}: {steps} transitions in {dt:.1f}s")
70
- print(f" {instr / dt / 1e6:,.1f} million instructions/s aggregate "
71
- f"({instr:,} instructions retired)")
72
- print(f" {instr * n_neurons / dt / 1e9:,.1f} billion threshold-neuron "
73
- f"evaluations/s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_reflect_self_modifying_stack.py DELETED
@@ -1,87 +0,0 @@
1
- """neural_reflect -- three levels of machine in one tensor.
2
-
3
- Level 0 is U, a fixed ternary threshold interpreter (~24k gates). Level 1 is a
4
- complete SUBLEQ computer stored as data inside U's writable state. Level 2 is a
5
- SUBLEQ program that rewrites its own operand bytes as it runs: a zeroing
6
- instruction that walks itself across memory, erasing a block. The interpreter,
7
- the machine it runs, and the program that machine executes are all the same
8
- tensor; the final memory is checked bit-for-bit against a native SUBLEQ emulator.
9
-
10
- python demos/neural_reflect_self_modifying_stack.py
11
- """
12
- import os, sys, time
13
- HERE = os.path.dirname(os.path.abspath(__file__))
14
- REPO = os.path.dirname(HERE)
15
- sys.path.insert(0, os.path.join(REPO, "src"))
16
- import torch
17
- from reflect import (Cfg, build_subleq_machine, build_net, Leveled,
18
- encode_netlist, state_to_vec, subleq32_step, MEM, MEMB)
19
-
20
-
21
- if __name__ == "__main__":
22
- dev = "cuda" if torch.cuda.is_available() else "cpu"
23
- cfg = Cfg(A=14, G=296, banks=1, self_mod=False)
24
- prog, lay = build_subleq_machine(cfg)
25
- inet, ii, io = build_net(cfg)
26
- print("neural_reflect: self-modifying code on a stored machine on U")
27
- print("=" * 60)
28
- print(f"level 0: interpreter U = {len(inet.gates):,} threshold gates (fixed)")
29
- print(f"level 1: a SUBLEQ machine = {len(prog)} gates, stored as writable data")
30
- lev = Leveled(inet, ii, io, device=dev)
31
- nl = encode_netlist(cfg, prog)
32
- sig0 = [0] * cfg.S
33
- sig0[cfg.NET0:cfg.NET0 + len(nl)] = nl
34
- base = state_to_vec(cfg, {"sig": sig0, "gp": 0, "halt": 0}).to(dev)
35
- PC = lay["PC"]
36
-
37
- # level 2: SUBLEQ (A B C: M[B]-=M[A]; if <=0 goto C). The first instruction
38
- # zeroes a target cell, then two instructions increment its own A and B
39
- # operand bytes, so the zeroing walks across a data block.
40
- pm = [0] * 32
41
- pm[0:3] = [24, 24, 3] # zero M[<target>]; these operands get rewritten
42
- pm[3:6] = [19, 0, 6] # M[0] += 1 (patch own A-field: cell 0)
43
- pm[6:9] = [19, 1, 9] # M[1] += 1 (patch own B-field: cell 1)
44
- pm[9:12] = [20, 21, 15] # count -= 1; if <= 0 branch to epilogue
45
- pm[12:15] = [18, 18, 0] # 0 -> unconditional jump to loop head
46
- pm[15:18] = [18, 18, 31] # epilogue: branch to 31 = HALT
47
- pm[18], pm[19], pm[20], pm[21] = 0, 255, 1, 4
48
- pm[24:28] = [7, 99, 123, 200] # the block the program will erase
49
- print(f"level 2: a program whose instruction 0 starts as [24,24,3] and "
50
- f"rewrites its own operands")
51
-
52
- ref_mem, ref_pc = list(pm), 0
53
- while ref_pc != 31:
54
- ref_mem, ref_pc = subleq32_step(ref_mem, ref_pc)
55
-
56
- V = base.unsqueeze(0).clone()
57
- for i in range(32):
58
- for b in range(8):
59
- V[:, MEM + b * MEMB + i] = float((pm[i] >> b) & 1)
60
-
61
- def get_mem(Vc):
62
- return sum(Vc[:, MEM + b * MEMB: MEM + b * MEMB + 32].long() << b
63
- for b in range(8))[0].tolist()
64
-
65
- t0, walk, steps, halted = time.perf_counter(), [], 0, False
66
- for _ in range(26):
67
- m_now = get_mem(V.cpu())
68
- walk.append((m_now[0], m_now[1]))
69
- for _ in range(cfg.G):
70
- V = lev.step(V)
71
- steps += 1
72
- if int(V[0, cfg.S + cfg.GPW]) == 1:
73
- halted = True
74
- break
75
- dt = time.perf_counter() - t0
76
- got = get_mem(V.cpu())
77
-
78
- mut = " -> ".join(f"[{a},{b}]" for a, b in
79
- [walk[0]] + [w for i, w in enumerate(walk[1:], 1) if w != walk[i - 1]])
80
- print(f"\nran {steps} hosted instructions in {dt:.0f}s "
81
- f"({steps * cfg.G} interpreter recurrences); halted={halted}")
82
- print(f" instruction 0's operand cells as it ran: {mut}")
83
- print(f" target block M[24..27]: {pm[24:28]} -> {got[24:28]}")
84
- print(f" final memory == native SUBLEQ reference: "
85
- f"{'EXACT' if got == ref_mem else 'MISMATCH'}")
86
- print("\n The program, the machine it runs on, and the machine that runs")
87
- print(" that machine are all the same tensor.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_reversible_counterfactual.py DELETED
@@ -1,49 +0,0 @@
1
- """neural_reversible -- computation with no erasure, and the counterfactual.
2
-
3
- Every instruction of this register machine is a bijection, so the same weights
4
- run backward invert the forward computation: a mixing program hashes a seed into
5
- a digest, and stepping in reverse reconstructs the seed exactly, with no bit ever
6
- erased (Landauer floor: zero). Because history is only consistent with the truth,
7
- flipping a single bit of the digest and running backward yields a "past" that is
8
- wrong in many bits: information is conserved, so a corrupted future refuses to
9
- invert to the real input.
10
-
11
- python demos/neural_reversible_counterfactual.py
12
- """
13
- import os, sys
14
- HERE = os.path.dirname(os.path.abspath(__file__))
15
- REPO = os.path.dirname(HERE)
16
- sys.path.insert(0, os.path.join(REPO, "src"))
17
- from reversible_cpu import RCPU, _clone
18
-
19
-
20
- if __name__ == "__main__":
21
- # cross-coupled mixing: R0 feeds R3 and R3 feeds R0, so the avalanche spreads
22
- prog = []
23
- for _ in range(8):
24
- prog += [("ADD", 0, 3), ("TOFF", 3, 0, 2), ("ROL", 0, 1), ("XOR", 2, 3)]
25
- cpu = RCPU(prog)
26
- seed = cpu.new_state(regs=[7, 13, 201, 0])
27
-
28
- print("neural_reversible: no-erasure computing and the counterfactual")
29
- print("=" * 60)
30
- s = _clone(seed)
31
- cpu.run(s, len(prog))
32
- print(f"seed registers {seed['R']} --{len(prog)} bijective steps--> "
33
- f"digest {s['R']}")
34
-
35
- b = _clone(s)
36
- cpu.run_back(b, len(prog))
37
- print(f"run backward: seed recovered "
38
- f"{'EXACTLY' if b['R'] == seed['R'] and b['PC'] == seed['PC'] else 'FAILED'} "
39
- f"(no bit was ever erased; Landauer floor 0)")
40
-
41
- c = _clone(s)
42
- c["R"][3] ^= 4 # corrupt one bit of the digest
43
- cpu.run_back(c, len(prog))
44
- diff = sum(bin(x ^ y).count("1") for x, y in zip(c["R"], seed["R"]))
45
- print(f"counterfactual: flip ONE digest bit, run history backward -> the "
46
- f"reconstructed 'seed' is wrong in {diff}/32 register bits")
47
- print(f" corrupted 'seed' = {c['R']} true seed = {seed['R']}")
48
- print("\n History inverts only when it is the true history. A tampered")
49
- print(" future does not lead back to the real past.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_rv32_machin_pi.py DELETED
@@ -1,85 +0,0 @@
1
- """neural_rv32 -- pi by Machin's formula, computed on the RISC-V threshold CPU.
2
-
3
- pi = 16*arctan(1/5) - 4*arctan(1/239) (John Machin, 1706), evaluated as an
4
- integer arctangent series at scale 1e8 with rounded divisions. Every add,
5
- multiply and divide runs through the ternary threshold datapath; 239^2 is
6
- computed on-machine, and the result is formatted to the memory-mapped console.
7
-
8
- python demos/neural_rv32_machin_pi.py
9
- """
10
- import os, sys, time
11
- HERE = os.path.dirname(os.path.abspath(__file__))
12
- REPO = os.path.dirname(HERE)
13
- sys.path.insert(0, os.path.join(REPO, "src"))
14
- import torch
15
- from machines import Asm, Rv32ThresholdCPU, rv_state
16
- from eval_all import int_to_bits
17
-
18
-
19
- def native():
20
- """Integer mirror of the exact recurrence the machine will run."""
21
- accA, t, sign, d, dsA = 0, 1600000000 // 5, 1, 1, []
22
- while t > 0:
23
- accA += sign * ((t + d // 2) // d); dsA.append(d)
24
- sign, d, t = -sign, d + 2, (t + 12) // 25
25
- accB, t, q, dsB, sign, d = 0, (400000000 + 119) // 239, 239 * 239, [], 1, 1
26
- while t > 0:
27
- accB += sign * ((t + d // 2) // d); dsB.append(d)
28
- sign, d, t = -sign, d + 2, (t + q // 2) // q
29
- return accA - accB, dsA, dsB
30
-
31
-
32
- def build(dsA, dsB):
33
- a = Asm()
34
- a.lui(4, 0x5F5E1) # x4 = 1,600,000,000
35
- a.addi(5, 0, 25); a.addi(6, 0, 10); a.addi(8, 0, 239); a.addi(14, 0, 5)
36
- a.div(11, 4, 14) # t = 16e8/5
37
- a.addi(10, 0, 0) # accA
38
- for k, d in enumerate(dsA):
39
- a.addi(12, 0, d); a.addi(13, 11, d // 2); a.div(13, 13, 12)
40
- (a.add if k % 2 == 0 else a.sub)(10, 10, 13)
41
- if k + 1 < len(dsA):
42
- a.addi(13, 11, 12); a.div(11, 13, 5)
43
- a.lui(7, 0x17D78); a.ori(7, 7, 0x400) # x7 = 400,000,000
44
- a.addi(13, 7, 119); a.div(11, 13, 8)
45
- a.mul(9, 8, 8) # 239^2 computed on-machine
46
- a.srli(14, 9, 1)
47
- a.addi(15, 0, 0) # accB
48
- for k, d in enumerate(dsB):
49
- a.addi(12, 0, d); a.addi(13, 11, d // 2); a.div(13, 13, 12)
50
- (a.add if k % 2 == 0 else a.sub)(15, 15, 13)
51
- if k + 1 < len(dsB):
52
- a.add(13, 11, 14); a.div(11, 13, 9)
53
- a.sub(10, 10, 15) # x10 = pi * 1e8
54
- a.addi(20, 0, 0x7F8); a.slli(20, 20, 5) # console 0xFF00
55
- a.lui(21, 0x5F5E); a.ori(21, 21, 0x100) # P = 1e8
56
- a.div(22, 10, 21); a.rem(10, 10, 21)
57
- a.addi(22, 22, 48); a.sb(22, 20, 0) # integer digit
58
- a.addi(22, 0, 46); a.sb(22, 20, 0) # '.'
59
- a.div(21, 21, 6); a.addi(23, 0, 8)
60
- a.label("digits")
61
- a.div(22, 10, 21); a.rem(10, 10, 21)
62
- a.addi(22, 22, 48); a.sb(22, 20, 0)
63
- a.div(21, 21, 6); a.addi(23, 23, -1); a.bne(23, 0, "digits")
64
- a.ecall()
65
- return a.assemble()
66
-
67
-
68
- if __name__ == "__main__":
69
- pi9, dsA, dsB = native()
70
- mem = build(dsA, dsB)
71
- print("neural_rv32: Machin's pi on the threshold datapath")
72
- print("=" * 56)
73
- print("loading neural_rv32 (8.77M ternary parameters)...")
74
- cpu = Rv32ThresholdCPU()
75
- ts = rv_state(mem)
76
- ts["_mem_bits"] = torch.tensor([int_to_bits(b, 8) for b in ts["mem"]], dtype=torch.float32)
77
- t0, cyc = time.perf_counter(), 0
78
- while not ts["halted"] and cyc < 400:
79
- ts = cpu.step(ts); cyc += 1
80
- want = f"{pi9 // 10**8}.{pi9 % 10**8:08d}"
81
- print(f"console output : {ts['console']!r} ({cyc} cycles, "
82
- f"{time.perf_counter()-t0:.0f}s, dual-issue pairs {cpu.pairs_issued})")
83
- print(f"integer mirror : {want!r} (machine {'EXACT' if ts['console']==want else 'MISMATCH'})")
84
- print(f"true pi : 3.14159265(35...) -> all printed digits "
85
- f"{'correct' if ts['console']=='3.14159265' else 'NOT all correct'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_rv32_neural_nets_via_neur.py DELETED
@@ -1,184 +0,0 @@
1
- """neural_rv32 -- neural networks running as software on the neural-net CPU.
2
-
3
- neural_rv32's NEUR opcode computes exactly one ternary threshold neuron:
4
- rd = H( popcount(rs1 & pos) - popcount(rs1 & neg) + sext5(bias) )
5
- with weights in {-1,0,+1} and an integer bias. So a program of NEUR
6
- instructions IS a ternary neural network -- and the CPU executing it is itself
7
- a threshold network. One net here is LEARNED by gradient descent (a nonlinear
8
- popcount band) and one is COMPILED by construction (8-bit parity, the function
9
- that ended the single perceptron); both are then deployed as NEUR code and run
10
- on the threshold datapath, with the CPU output checked against the reference net.
11
-
12
- python demos/neural_rv32_neural_nets_via_neur.py
13
- """
14
- import os, sys, time
15
- HERE = os.path.dirname(os.path.abspath(__file__))
16
- REPO = os.path.dirname(HERE)
17
- sys.path.insert(0, os.path.join(REPO, "src"))
18
- os.chdir(REPO)
19
- import torch
20
- from machines import Asm, Rv32ThresholdCPU, rv_state
21
- from eval_all import int_to_bits
22
-
23
- torch.manual_seed(0)
24
-
25
-
26
- def ternary(w): # {-1,0,+1} forward, identity backward (STE)
27
- wq = torch.where(w > 0.5, 1.0, torch.where(w < -0.5, -1.0, 0.0))
28
- return w + (wq - w).detach()
29
-
30
- def ibias(b, lo=-16, hi=15):
31
- bq = b.round().clamp(lo, hi)
32
- return b + (bq - b).detach()
33
-
34
- # ---- discrete NEUR reference (exactly what the hardware computes) ----------
35
- def neuron(x, pos, neg, bias):
36
- return 1 if (bin(x & pos).count("1") - bin(x & neg).count("1") + bias) >= 0 else 0
37
-
38
- def discrete_net(x, hidden, out):
39
- h = 0
40
- for j, (p, n, b) in enumerate(hidden):
41
- h |= neuron(x, p, n, b) << j
42
- p, n, b = out
43
- return neuron(h, p, n, b)
44
-
45
- # ---- train a 2-layer ternary MLP; hidden step trained with an annealed -----
46
- # sigmoid so real gradients flow, then hardened to Heaviside at extraction
47
- def train(target_fn, H, epochs=5000, seed=0, nbits=8):
48
- torch.manual_seed(seed)
49
- N = 1 << nbits
50
- xs = torch.arange(N)
51
- X = torch.stack([((xs >> i) & 1).float() for i in range(nbits)], 1)
52
- Y = torch.tensor([float(target_fn(int(x))) for x in xs])
53
- Wh = torch.nn.Parameter(torch.randn(H, nbits) * 1.5)
54
- bh = torch.nn.Parameter(torch.randn(H) * 1.5)
55
- Wo = torch.nn.Parameter(torch.randn(H) * 1.5)
56
- bo = torch.nn.Parameter(torch.zeros(1))
57
- opt = torch.optim.Adam([Wh, bh, Wo, bo], lr=0.03)
58
- npos = float(Y.sum())
59
- bce = torch.nn.BCEWithLogitsLoss(pos_weight=torch.tensor((N - npos) / max(npos, 1.0)))
60
- for ep in range(epochs):
61
- opt.zero_grad()
62
- Tt = 1.0 + 11.0 * ep / epochs
63
- preh = X @ ternary(Wh).T + ibias(bh)
64
- h = torch.sigmoid(Tt * preh)
65
- preo = h @ ternary(Wo) + ibias(bo)
66
- loss = bce(4.0 * preo, Y)
67
- loss.backward(); opt.step()
68
- Whq = torch.where(Wh > 0.5, 1, torch.where(Wh < -0.5, -1, 0)).tolist()
69
- Woq = torch.where(Wo > 0.5, 1, torch.where(Wo < -0.5, -1, 0)).tolist()
70
- bhq = bh.round().clamp(-16, 15).long().tolist()
71
- boq = int(bo.round().clamp(-16, 15).item())
72
- def pack(wrow):
73
- return (sum(1 << i for i, w in enumerate(wrow) if w > 0),
74
- sum(1 << i for i, w in enumerate(wrow) if w < 0))
75
- hidden = [(*pack(Whq[j]), bhq[j]) for j in range(H)]
76
- out = (*pack(Woq), boq)
77
- acc = sum(discrete_net(int(x), hidden, out) == int(Y[i]) for i, x in enumerate(xs)) / N
78
- return hidden, out, acc
79
-
80
- # ---- emit the network as NEUR code and run it on neural_rv32 ---------------
81
- def li(a, reg, v):
82
- v &= 0xFFFFFFFF
83
- hi = (v + 0x800) >> 12
84
- lo = v - (hi << 12)
85
- a.lui(reg, hi & 0xFFFFF)
86
- if lo: a.addi(reg, reg, lo)
87
-
88
- def build_program(xval, hidden, out):
89
- a = Asm()
90
- li(a, 1, xval) # x1 = input byte
91
- li(a, 2, 0) # x2 = packed hidden
92
- for j, (p, n, b) in enumerate(hidden):
93
- li(a, 4, p | (n << 8) | ((b & 0x1F) << 16)) # weight word
94
- a.neur(3, 1, 4) # x3 = hidden neuron j
95
- if j: a.slli(2, 2, 1)
96
- a.or_(2, 2, 3) # pack MSB-first (neuron 0 -> bit H-1)
97
- p, n, b = out
98
- H = len(hidden)
99
- def remap(mask): # match the MSB-first hidden packing
100
- r = 0
101
- for j in range(H):
102
- if (mask >> j) & 1: r |= 1 << (H - 1 - j)
103
- return r
104
- li(a, 4, remap(p) | (remap(n) << 8) | ((b & 0x1F) << 16))
105
- a.neur(5, 2, 4) # x5 = output
106
- a.ecall()
107
- return a.assemble()
108
-
109
- def run_cpu(cpu, xval, hidden, out):
110
- mem = build_program(xval, hidden, out)
111
- ts = rv_state(mem)
112
- ts["_mem_bits"] = torch.tensor([int_to_bits(b, 8) for b in ts["mem"]], dtype=torch.float32)
113
- cyc = 0
114
- while not ts["halted"] and cyc < 200:
115
- ts = cpu.step(ts); cyc += 1
116
- return ts["regs"][5], cyc
117
-
118
-
119
- def parity(x, nbits=8): return bin(x & ((1 << nbits) - 1)).count("1") & 1
120
- def band(x): return int(3 <= bin(x).count("1") <= 5) # non-monotone in popcount
121
-
122
- def learn(fn, H, seeds, epochs):
123
- best, t0, used = None, time.perf_counter(), 0
124
- for seed in range(seeds):
125
- used = seed
126
- hidden, out, acc = train(fn, H=H, seed=seed, epochs=epochs)
127
- if best is None or acc > best[2]:
128
- best = (hidden, out, acc)
129
- if acc == 1.0:
130
- break
131
- hidden, out, acc = best
132
- print(f" trained 8->{H}->1 ternary MLP by annealed-sigmoid SGD: {acc*100:.1f}% "
133
- f"exact over all 256 inputs ({time.perf_counter()-t0:.0f}s, seed {used}); "
134
- f"weights all in {{-1,0,+1}}")
135
- return hidden, out, acc
136
-
137
- def deploy_check(cpu, fn, hidden, out, sample):
138
- t0, cpu_vs_net, cpu_vs_true, rows = time.perf_counter(), 0, 0, []
139
- for x in sample:
140
- y_cpu, _ = run_cpu(cpu, x, hidden, out)
141
- cpu_vs_net += (y_cpu == discrete_net(x, hidden, out))
142
- cpu_vs_true += (y_cpu == fn(x))
143
- rows.append((x, y_cpu, fn(x)))
144
- print(f" NEUR execution on neural_rv32: CPU == reference net on "
145
- f"{cpu_vs_net}/{len(sample)} (deployment exact); CPU == truth on "
146
- f"{cpu_vs_true}/{len(sample)} ({time.perf_counter()-t0:.0f}s)")
147
- return rows
148
-
149
- def compile_parity(nbits=8): # exact thermometer construction
150
- mask = (1 << nbits) - 1
151
- hidden = [(mask, 0, -(k + 1)) for k in range(nbits)] # h_k = [popcount>=k+1]
152
- pos = sum(1 << j for j in range(nbits) if j % 2 == 0) # odd thresholds
153
- neg = sum(1 << j for j in range(nbits) if j % 2 == 1) # even thresholds
154
- return hidden, (pos, neg, -1)
155
-
156
-
157
- if __name__ == "__main__":
158
- print("=" * 72)
159
- print(" Neural networks running as software on the CPU that IS a neural net")
160
- print("=" * 72)
161
- cpu = Rv32ThresholdCPU()
162
-
163
- print("\n[1] LEARNED by gradient descent -- popcount BAND: fire iff 3<=popcount<=5")
164
- print(" (non-monotone, so no single threshold can do it; genuinely nonlinear)")
165
- hidden, out, acc = learn(band, H=8, seeds=12, epochs=5000)
166
- deploy_check(cpu, band, hidden, out,
167
- [0, 1, 3, 7, 15, 31, 63, 127, 255, 0b10110, 0b1111000, 0b11011011])
168
- line = "".join(f"{k}:{discrete_net((1 << k) - 1, hidden, out)} " for k in range(9))
169
- print(f" net output vs popcount k: {line.strip()} (true band: k in 3,4,5)")
170
-
171
- print("\n[2] COMPILED by construction -- 8-bit PARITY (XOR), the non-separable")
172
- print(" function Minsky & Papert used to end the single perceptron")
173
- hidden, out = compile_parity(8)
174
- acc = sum(discrete_net(x, hidden, out) == parity(x) for x in range(256)) / 256
175
- print(f" thermometer net (8 hidden 'popcount>=k' + alternating output), all "
176
- f"weights in {{-1,0,+1}}: {acc*100:.0f}% exact over all 256 bytes")
177
- rows = deploy_check(cpu, parity, hidden, out,
178
- [0, 1, 3, 7, 15, 31, 85, 127, 128, 170, 200, 254, 255])
179
- for x, yc, yt in rows:
180
- print(f" parity({x:3d}) = {yc} (true {yt}) [{'ok' if yc==yt else 'x'}]")
181
-
182
- print("\n One network learned by SGD, one compiled by hand, both executed")
183
- print(" instruction by instruction on a CPU whose every gate is a threshold")
184
- print(" neuron. The machine running the models belongs to the models' own class.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_subleq8io_universal_constructor.py DELETED
@@ -1,74 +0,0 @@
1
- """neural_subleq8io -- von Neumann's universal constructor, to the byte.
2
-
3
- The one-instruction machine (186 datapath units plus 8 packed memory
4
- tensors) runs a 21-instruction SUBLEQ program
5
- that reads a description of any machine in the family off its tape and emits
6
- that machine's safetensors file one byte at a time. Here it fabricates
7
- neural_tile.safetensors on the shipped gate netlist, byte-for-byte sha-identical
8
- to the target, and the fabricated file is then booted: tiles reconstructed from
9
- its own bytes regrow the binary counter. A computer made of threshold gates
10
- prints a working computer, and the product runs.
11
-
12
- python demos/neural_subleq8io_universal_constructor.py
13
- """
14
- import os, sys, time, json, hashlib
15
- HERE = os.path.dirname(os.path.abspath(__file__))
16
- REPO = os.path.dirname(HERE)
17
- sys.path.insert(0, os.path.join(REPO, "src"))
18
- os.chdir(REPO)
19
- from constructor8 import describe, ref_construct, GateHost
20
-
21
-
22
- if __name__ == "__main__":
23
- target_path = os.path.join(REPO, "variants", "neural_tile.safetensors")
24
- data = open(target_path, "rb").read()
25
- tape = describe(data)
26
- print("neural_subleq8io: the universal constructor fabricates a sibling")
27
- print("=" * 60)
28
- print(f"recipe compiled: {len(data)} B machine -> {len(tape)} B tape")
29
-
30
- out_ref, n_ref = ref_construct(tape)
31
- assert out_ref == data
32
- print(f"reference: constructor needs {n_ref:,} instructions")
33
-
34
- host = GateHost()
35
- t0 = time.perf_counter()
36
- out, n, _ = host.run(tape, max_steps=n_ref + 100)
37
- dt = time.perf_counter() - t0
38
- sha_out = hashlib.sha256(out).hexdigest()[:16]
39
- sha_tgt = hashlib.sha256(data).hexdigest()[:16]
40
- print(f"gate netlist: {n:,} instructions in {dt:.0f}s ({n / dt:.0f} instr/s), "
41
- f"{len(out)} bytes emitted")
42
- print(f" sha256 fabricated={sha_out} shipped={sha_tgt} "
43
- f"{'BYTE-IDENTICAL' if out == data else 'MISMATCH'}; "
44
- f"instruction count == reference: {n == n_ref}")
45
-
46
- # boot the fabricated machine: regrow its counter from its own bytes
47
- fab = os.path.join(os.environ.get("TEMP", "."), "fabricated_neural_tile.safetensors")
48
- open(fab, "wb").write(out)
49
- from safetensors.torch import load_file
50
- from safetensors import safe_open
51
- import tile as T
52
-
53
- t = load_file(fab)
54
- with safe_open(fab, framework="pt") as f:
55
- m = f.metadata()
56
- gl = json.loads(m["glues"])
57
- strg = {gl[i]: int(v) for i, v in enumerate(t["glue_strength"].tolist())}
58
- tiles = []
59
- for row, name in zip(t["tile_glues"].tolist(), json.loads(m["tile_names"])):
60
- sides = [gl[i] if i >= 0 else "" for i in row]
61
- tiles.append(T.Tile(N=sides[0], E=sides[1], S=sides[2], W=sides[3], name=name))
62
- NB = 8
63
- rows = (1 << NB) - 1
64
- A, det = T.grow(tiles, T.counter_seed(NB), int(m["tau"]), strg, (0, 0, NB, rows))
65
- bad = filled = 0
66
- for y in range(1, rows + 1):
67
- cells = [A.get((x, y)) for x in range(NB)]
68
- if any(cc is None for cc in cells):
69
- continue
70
- filled += 1
71
- v = sum((1 if cc.N == "b1" else 0) << (NB - 1 - x) for x, cc in enumerate(cells))
72
- bad += (v != y)
73
- print(f"boot: the fabricated file grew its counter, {filled} rows, "
74
- f"row y encodes y: {'EXACT' if bad == 0 and filled == rows else f'{bad} bad'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demos/neural_tile_pascal_lucas.py DELETED
@@ -1,42 +0,0 @@
1
- """neural_tile -- self-assembly grows Pascal's triangle mod 2 (Lucas' theorem).
2
-
3
- The program is a set of square tiles whose binding rule is a threshold gate: a
4
- tile attaches at a site when the summed strength of its matching glues meets the
5
- temperature. With the XOR rule tile set, the crystal that grows is exactly the
6
- Sierpinski triangle -- cell (x,y) is filled iff the binomial C(x+y, x) is odd,
7
- which by Lucas' theorem is iff x AND y == 0. Every filled cell is verified
8
- against that arithmetic. Computation as crystal growth.
9
-
10
- python demos/neural_tile_pascal_lucas.py
11
- """
12
- import os, sys, time
13
- HERE = os.path.dirname(os.path.abspath(__file__))
14
- REPO = os.path.dirname(HERE)
15
- sys.path.insert(0, os.path.join(REPO, "src"))
16
- import tile as T
17
-
18
-
19
- if __name__ == "__main__":
20
- N = 100
21
- print("neural_tile: Pascal mod 2 by threshold-gated self-assembly")
22
- print("=" * 60)
23
- t0 = time.perf_counter()
24
- ts = T.rule2_tileset(lambda w, s: w ^ s)
25
- seed = T._row_col_seed([1] * (N + 1), [1] * (N + 1))
26
- A, det = T.grow(ts, seed, tau=2, strength={}, bounds=(0, 0, N, N), max_tiles=200000)
27
- dt = time.perf_counter() - t0
28
-
29
- interior = [(x, y) for x in range(1, N + 1) for y in range(1, N + 1)]
30
- grown = sum(1 for p in interior if p in A)
31
- bad = 0
32
- for (x, y) in interior:
33
- v = 1 if A[(x, y)].N == "v1" else 0
34
- lucas = 1 if (x & y) == 0 else 0 # C(x+y,x) odd <=> no carry in x+y
35
- bad += (v != lucas)
36
- print(f"grew {grown} rule tiles in {dt:.1f}s (directed={det}, "
37
- f"{2 * N} anti-diagonals deep)")
38
- print(f"tile(x,y) == [C(x+y,x) is odd] for all {len(interior)} cells: "
39
- f"{'EXACT' if bad == 0 else f'{bad} MISMATCHES'}")
40
- print("\ncorner of the assembly (30x30, '#' = odd binomial):")
41
- for y in range(30, 0, -1):
42
- print(" " + "".join("#" if A[(x, y)].N == "v1" else "." for x in range(1, 31)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
llm_integration/baseline.py DELETED
@@ -1,221 +0,0 @@
1
- """
2
- Baseline evaluation: Vanilla SmolLM2-360M on arithmetic
3
- """
4
-
5
- import torch
6
- import random
7
- import re
8
- from transformers import AutoModelForCausalLM, AutoTokenizer
9
-
10
- DEVICE = "cuda"
11
- MODEL_ID = "HuggingFaceTB/SmolLM2-360M-Instruct"
12
-
13
- SYSTEM_PROMPT = """You are a calculator. Output only the numeric answer. No words, no explanation, just digits. Examples:
14
- User: 5 + 3
15
- Assistant: 8
16
- User: 12 * 7
17
- Assistant: 84
18
- User: 100 > 50
19
- Assistant: 1
20
- User: 25 < 10
21
- Assistant: 0"""
22
-
23
-
24
- def load_model():
25
- print(f"Loading {MODEL_ID}...")
26
- tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
27
- tokenizer.padding_side = "left"
28
- if tokenizer.pad_token is None:
29
- tokenizer.pad_token = tokenizer.eos_token
30
- model = AutoModelForCausalLM.from_pretrained(
31
- MODEL_ID,
32
- torch_dtype=torch.float16,
33
- device_map=DEVICE
34
- )
35
- model.eval()
36
- print(f" Loaded. Parameters: {sum(p.numel() for p in model.parameters()):,}")
37
- return model, tokenizer
38
-
39
-
40
- def format_prompt(tokenizer, op_str):
41
- messages = [
42
- {"role": "system", "content": SYSTEM_PROMPT},
43
- {"role": "user", "content": op_str}
44
- ]
45
- return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
46
-
47
-
48
- def generate_batch(model, tokenizer, prompts, max_new_tokens=16):
49
- inputs = tokenizer(prompts, return_tensors="pt", padding=True).to(DEVICE)
50
- with torch.no_grad():
51
- outputs = model.generate(
52
- **inputs,
53
- max_new_tokens=max_new_tokens,
54
- do_sample=False,
55
- pad_token_id=tokenizer.eos_token_id
56
- )
57
- responses = []
58
- for i, output in enumerate(outputs):
59
- response = tokenizer.decode(output[inputs.input_ids.shape[1]:], skip_special_tokens=True)
60
- responses.append(response.strip())
61
- return responses
62
-
63
-
64
- def extract_answer(text):
65
- """Generous extraction - find any number in output"""
66
- text = text.strip().lower()
67
- if not text:
68
- return None
69
-
70
- # Handle Yes/No for comparisons
71
- if text in ['yes', 'true', '1']:
72
- return 1
73
- if text in ['no', 'false', '0']:
74
- return 0
75
- if text.startswith('yes'):
76
- return 1
77
- if text.startswith('no'):
78
- return 0
79
-
80
- # Find all numbers, take the LAST one (most likely the answer)
81
- numbers = re.findall(r'-?\d+', text)
82
- if numbers:
83
- return int(numbers[-1])
84
- return None
85
-
86
-
87
- def ground_truth(a, b, op):
88
- """Compute expected result (8-bit where applicable)"""
89
- if op == 'add':
90
- return (a + b) & 0xFF
91
- elif op == 'sub':
92
- return (a - b) & 0xFF
93
- elif op == 'mul':
94
- return (a * b) & 0xFF
95
- elif op == 'div':
96
- return a // b if b != 0 else 0
97
- elif op == 'and':
98
- return a & b
99
- elif op == 'or':
100
- return a | b
101
- elif op == 'xor':
102
- return a ^ b
103
- elif op == 'gt':
104
- return 1 if a > b else 0
105
- elif op == 'lt':
106
- return 1 if a < b else 0
107
- elif op == 'eq':
108
- return 1 if a == b else 0
109
- elif op == 'ge':
110
- return 1 if a >= b else 0
111
- elif op == 'le':
112
- return 1 if a <= b else 0
113
- else:
114
- raise ValueError(f"Unknown op: {op}")
115
-
116
-
117
- def op_to_str(a, b, op):
118
- """Convert operation to natural string"""
119
- symbols = {
120
- 'add': '+', 'sub': '-', 'mul': '*', 'div': '/',
121
- 'and': '&', 'or': '|', 'xor': '^',
122
- 'gt': '>', 'lt': '<', 'eq': '==', 'ge': '>=', 'le': '<='
123
- }
124
- return f"{a} {symbols[op]} {b}"
125
-
126
-
127
- def evaluate(model, tokenizer, n_samples=1000, batch_size=32, ops=None):
128
- if ops is None:
129
- ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
130
-
131
- results = {op: {'correct': 0, 'total': 0} for op in ops}
132
- all_correct = 0
133
- all_total = 0
134
-
135
- samples = []
136
- for _ in range(n_samples):
137
- a = random.randint(0, 255)
138
- b = random.randint(0, 255)
139
- if 'div' in ops and random.random() < 0.1:
140
- op = 'div'
141
- b = random.randint(1, 255) # avoid div by zero
142
- else:
143
- op = random.choice([o for o in ops if o != 'div'])
144
- samples.append((a, b, op))
145
-
146
- print(f"\nEvaluating {n_samples} samples (batch_size={batch_size})...")
147
-
148
- for batch_start in range(0, n_samples, batch_size):
149
- batch = samples[batch_start:batch_start + batch_size]
150
- prompts = [format_prompt(tokenizer, op_to_str(a, b, op)) for a, b, op in batch]
151
- responses = generate_batch(model, tokenizer, prompts)
152
-
153
- for (a, b, op), response in zip(batch, responses):
154
- expected = ground_truth(a, b, op)
155
- extracted = extract_answer(response)
156
-
157
- correct = (extracted == expected)
158
- results[op]['total'] += 1
159
- all_total += 1
160
- if correct:
161
- results[op]['correct'] += 1
162
- all_correct += 1
163
-
164
- if (batch_start + batch_size) % 200 == 0 or batch_start + batch_size >= n_samples:
165
- pct = 100 * all_correct / all_total
166
- print(f" Progress: {min(batch_start + batch_size, n_samples)}/{n_samples} | Accuracy: {pct:.2f}%")
167
-
168
- return results, all_correct, all_total
169
-
170
-
171
- def main():
172
- random.seed(42)
173
- torch.manual_seed(42)
174
-
175
- model, tokenizer = load_model()
176
-
177
- # Quick sanity check
178
- print("\nSanity check (5 examples):")
179
- test_cases = [
180
- ("5 + 3", 8),
181
- ("100 - 37", 63),
182
- ("12 * 11", 132),
183
- ("50 > 30", 1),
184
- ("25 < 10", 0),
185
- ]
186
- prompts = [format_prompt(tokenizer, q) for q, _ in test_cases]
187
- responses = generate_batch(model, tokenizer, prompts)
188
- for (q, expected), response in zip(test_cases, responses):
189
- extracted = extract_answer(response)
190
- status = "OK" if extracted == expected else "FAIL"
191
- print(f" {q} = {expected} | Model: '{response}' -> {extracted} [{status}]")
192
-
193
- # Full evaluation
194
- print("\n" + "=" * 60)
195
- print(" BASELINE EVALUATION")
196
- print("=" * 60)
197
-
198
- ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
199
- results, correct, total = evaluate(model, tokenizer, n_samples=2000, batch_size=64, ops=ops)
200
-
201
- print("\n" + "=" * 60)
202
- print(" RESULTS BY OPERATION")
203
- print("=" * 60)
204
- for op in ops:
205
- r = results[op]
206
- pct = 100 * r['correct'] / r['total'] if r['total'] > 0 else 0
207
- print(f" {op:6}: {r['correct']:4}/{r['total']:4} ({pct:6.2f}%)")
208
-
209
- print("\n" + "=" * 60)
210
- print(" OVERALL")
211
- print("=" * 60)
212
- fitness = correct / total
213
- print(f" Correct: {correct}/{total}")
214
- print(f" Fitness: {fitness:.4f} ({100*fitness:.2f}%)")
215
- print("=" * 60)
216
-
217
- return fitness
218
-
219
-
220
- if __name__ == "__main__":
221
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
llm_integration/circuits.py DELETED
@@ -1,320 +0,0 @@
1
- """
2
- Frozen threshold circuit wrapper for LLM integration.
3
- Loads safetensors and provides differentiable-compatible execution.
4
- """
5
-
6
- import torch
7
- import torch.nn as nn
8
- from safetensors import safe_open
9
- from typing import Dict, Tuple
10
-
11
- MODEL_PATH = "D:/8bit-threshold-computer/neural_computer.safetensors"
12
-
13
-
14
- def heaviside(x: torch.Tensor) -> torch.Tensor:
15
- """Standard Heaviside step function."""
16
- return (x >= 0).float()
17
-
18
-
19
- class HeavisideSTE(torch.autograd.Function):
20
- """Heaviside with straight-through estimator for gradients."""
21
- @staticmethod
22
- def forward(ctx, x):
23
- return (x >= 0).float()
24
-
25
- @staticmethod
26
- def backward(ctx, grad_output):
27
- return grad_output
28
-
29
-
30
- def heaviside_ste(x: torch.Tensor) -> torch.Tensor:
31
- """Heaviside with STE gradient."""
32
- return HeavisideSTE.apply(x)
33
-
34
-
35
- class FrozenThresholdCircuits(nn.Module):
36
- """
37
- Wrapper for frozen threshold logic circuits.
38
- All weights are frozen - no gradients flow through circuit internals.
39
- Gradients flow through inputs/outputs via STE.
40
- """
41
-
42
- def __init__(self, model_path: str = MODEL_PATH, device: str = 'cuda'):
43
- super().__init__()
44
- self.device = device
45
- self.weights = {}
46
- self._load_weights(model_path)
47
-
48
- def _load_weights(self, path: str):
49
- """Load weights from safetensors file."""
50
- with safe_open(path, framework='pt') as f:
51
- for name in f.keys():
52
- tensor = f.get_tensor(name).to(self.device).float()
53
- self.weights[name] = tensor
54
-
55
- def _gate(self, inputs: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor) -> torch.Tensor:
56
- """Execute single threshold gate with STE."""
57
- weight = weight.view(-1)
58
- bias = bias.view(-1)
59
- pre_activation = (inputs * weight).sum(dim=-1) + bias
60
- return heaviside_ste(pre_activation)
61
-
62
- def _xor(self, a: torch.Tensor, b: torch.Tensor, prefix: str) -> torch.Tensor:
63
- """XOR via OR-NAND-AND pattern (2 layers)."""
64
- inputs = torch.stack([a, b], dim=-1)
65
-
66
- w_or = self.weights[f'{prefix}.layer1.or.weight']
67
- b_or = self.weights[f'{prefix}.layer1.or.bias']
68
- w_nand = self.weights[f'{prefix}.layer1.nand.weight']
69
- b_nand = self.weights[f'{prefix}.layer1.nand.bias']
70
-
71
- h_or = self._gate(inputs, w_or, b_or)
72
- h_nand = self._gate(inputs, w_nand, b_nand)
73
-
74
- hidden = torch.stack([h_or, h_nand], dim=-1)
75
- w2 = self.weights[f'{prefix}.layer2.weight']
76
- b2 = self.weights[f'{prefix}.layer2.bias']
77
-
78
- return self._gate(hidden, w2, b2)
79
-
80
- def _full_adder(self, a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor,
81
- prefix: str) -> Tuple[torch.Tensor, torch.Tensor]:
82
- """Full adder: sum and carry out."""
83
- ha1_sum = self._xor(a, b, f'{prefix}.ha1.sum')
84
-
85
- inp_carry1 = torch.stack([a, b], dim=-1)
86
- w_c1 = self.weights[f'{prefix}.ha1.carry.weight']
87
- b_c1 = self.weights[f'{prefix}.ha1.carry.bias']
88
- ha1_carry = self._gate(inp_carry1, w_c1, b_c1)
89
-
90
- ha2_sum = self._xor(ha1_sum, cin, f'{prefix}.ha2.sum')
91
-
92
- inp_carry2 = torch.stack([ha1_sum, cin], dim=-1)
93
- w_c2 = self.weights[f'{prefix}.ha2.carry.weight']
94
- b_c2 = self.weights[f'{prefix}.ha2.carry.bias']
95
- ha2_carry = self._gate(inp_carry2, w_c2, b_c2)
96
-
97
- inp_cout = torch.stack([ha1_carry, ha2_carry], dim=-1)
98
- w_cout = self.weights[f'{prefix}.carry_or.weight']
99
- b_cout = self.weights[f'{prefix}.carry_or.bias']
100
- cout = self._gate(inp_cout, w_cout, b_cout)
101
-
102
- return ha2_sum, cout
103
-
104
- def add_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
105
- """
106
- 8-bit ripple carry addition.
107
-
108
- Args:
109
- a_bits: [batch, 8] MSB-first
110
- b_bits: [batch, 8] MSB-first
111
-
112
- Returns:
113
- result_bits: [batch, 8] MSB-first
114
- carry_out: [batch] final carry
115
- """
116
- batch_size = a_bits.shape[0]
117
- carry = torch.zeros(batch_size, device=self.device)
118
- result_bits = []
119
-
120
- for bit in range(8):
121
- bit_idx = 7 - bit
122
- s, carry = self._full_adder(
123
- a_bits[:, bit_idx],
124
- b_bits[:, bit_idx],
125
- carry,
126
- f'arithmetic.ripplecarry8bit.fa{bit}'
127
- )
128
- result_bits.insert(0, s)
129
-
130
- result = torch.stack(result_bits, dim=1)
131
- return result, carry
132
-
133
- def sub_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
134
- """
135
- 8-bit subtraction via two's complement: A - B = A + (~B) + 1
136
-
137
- Args:
138
- a_bits: [batch, 8] MSB-first
139
- b_bits: [batch, 8] MSB-first
140
-
141
- Returns:
142
- result_bits: [batch, 8] MSB-first
143
- borrow_out: [batch] (inverted carry)
144
- """
145
- b_inv = 1.0 - b_bits
146
- batch_size = a_bits.shape[0]
147
- carry = torch.ones(batch_size, device=self.device)
148
- result_bits = []
149
-
150
- for bit in range(8):
151
- bit_idx = 7 - bit
152
- s, carry = self._full_adder(
153
- a_bits[:, bit_idx],
154
- b_inv[:, bit_idx],
155
- carry,
156
- f'arithmetic.ripplecarry8bit.fa{bit}'
157
- )
158
- result_bits.insert(0, s)
159
-
160
- result = torch.stack(result_bits, dim=1)
161
- borrow = 1.0 - carry
162
- return result, borrow
163
-
164
- def mul_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
165
- """
166
- 8-bit multiplication via shift-add (software implementation using adder circuits).
167
- Only keeps low 8 bits of result (matches 8-bit wrap behavior).
168
-
169
- Args:
170
- a_bits: [batch, 8] MSB-first
171
- b_bits: [batch, 8] MSB-first
172
-
173
- Returns:
174
- result_bits: [batch, 8] MSB-first (low 8 bits of product)
175
- """
176
- batch_size = a_bits.shape[0]
177
-
178
- acc = torch.zeros(batch_size, 8, device=self.device)
179
-
180
- for i in range(8):
181
- b_bit = b_bits[:, 7 - i]
182
- pp = a_bits * b_bit.unsqueeze(1)
183
-
184
- shifted_pp = torch.zeros(batch_size, 8, device=self.device)
185
- for j in range(8):
186
- dst_idx = j + i
187
- if dst_idx < 8:
188
- shifted_pp[:, 7 - dst_idx] = pp[:, 7 - j]
189
-
190
- acc, _ = self.add_8bit(acc, shifted_pp)
191
-
192
- return acc
193
-
194
- def compare_gt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
195
- """A > B comparison."""
196
- inputs = torch.cat([a_bits, b_bits], dim=-1)
197
- w = self.weights['arithmetic.greaterthan8bit.weight'].view(-1)
198
- b = self.weights['arithmetic.greaterthan8bit.bias'].view(-1)
199
- return heaviside_ste((inputs * w).sum(dim=-1) + b)
200
-
201
- def compare_lt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
202
- """A < B comparison."""
203
- inputs = torch.cat([a_bits, b_bits], dim=-1)
204
- w = self.weights['arithmetic.lessthan8bit.weight'].view(-1)
205
- b = self.weights['arithmetic.lessthan8bit.bias'].view(-1)
206
- return heaviside_ste((inputs * w).sum(dim=-1) + b)
207
-
208
- def compare_eq(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
209
- """A == B comparison (two-layer)."""
210
- inputs = torch.cat([a_bits, b_bits], dim=-1)
211
- prefix = 'arithmetic.equality8bit'
212
-
213
- w_geq = self.weights[f'{prefix}.layer1.geq.weight'].view(-1)
214
- b_geq = self.weights[f'{prefix}.layer1.geq.bias'].view(-1)
215
- w_leq = self.weights[f'{prefix}.layer1.leq.weight'].view(-1)
216
- b_leq = self.weights[f'{prefix}.layer1.leq.bias'].view(-1)
217
-
218
- h_geq = heaviside_ste((inputs * w_geq).sum(dim=-1) + b_geq)
219
- h_leq = heaviside_ste((inputs * w_leq).sum(dim=-1) + b_leq)
220
-
221
- hidden = torch.stack([h_geq, h_leq], dim=-1)
222
- w2 = self.weights[f'{prefix}.layer2.weight'].view(-1)
223
- b2 = self.weights[f'{prefix}.layer2.bias'].view(-1)
224
-
225
- return heaviside_ste((hidden * w2).sum(dim=-1) + b2)
226
-
227
- def forward(self, a_bits: torch.Tensor, b_bits: torch.Tensor,
228
- op_onehot: torch.Tensor) -> torch.Tensor:
229
- """
230
- Execute operation based on one-hot selector.
231
- Uses soft routing during training for gradient flow.
232
-
233
- Args:
234
- a_bits: [batch, 8] operand A
235
- b_bits: [batch, 8] operand B
236
- op_onehot: [batch, 6] one-hot operation selector
237
- [add, sub, mul, gt, lt, eq]
238
-
239
- Returns:
240
- result_bits: [batch, 8] result (comparisons in bit 7, rest zeros)
241
- """
242
- batch_size = a_bits.shape[0]
243
-
244
- add_result, _ = self.add_8bit(a_bits, b_bits)
245
- sub_result, _ = self.sub_8bit(a_bits, b_bits)
246
- mul_result = self.mul_8bit(a_bits, b_bits)
247
-
248
- gt_result = self.compare_gt(a_bits, b_bits)
249
- lt_result = self.compare_lt(a_bits, b_bits)
250
- eq_result = self.compare_eq(a_bits, b_bits)
251
-
252
- cmp_expanded = torch.zeros(batch_size, 8, device=self.device)
253
-
254
- gt_expanded = cmp_expanded.clone()
255
- gt_expanded[:, 7] = gt_result
256
-
257
- lt_expanded = cmp_expanded.clone()
258
- lt_expanded[:, 7] = lt_result
259
-
260
- eq_expanded = cmp_expanded.clone()
261
- eq_expanded[:, 7] = eq_result
262
-
263
- results = torch.stack([
264
- add_result,
265
- sub_result,
266
- mul_result,
267
- gt_expanded,
268
- lt_expanded,
269
- eq_expanded
270
- ], dim=1)
271
-
272
- op_weights = op_onehot.unsqueeze(-1)
273
- output = (results * op_weights).sum(dim=1)
274
-
275
- return output
276
-
277
-
278
- if __name__ == "__main__":
279
- print("Testing frozen circuits...")
280
-
281
- circuits = FrozenThresholdCircuits(device='cuda')
282
- print(f"Loaded {len(circuits.weights)} tensors")
283
-
284
- a = torch.tensor([[0, 0, 0, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
285
- b = torch.tensor([[0, 0, 0, 0, 0, 0, 1, 1]], device='cuda', dtype=torch.float32)
286
-
287
- result, carry = circuits.add_8bit(a, b)
288
- val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
289
- print(f"5 + 3 = {val} (expected 8)")
290
-
291
- a = torch.tensor([[0, 1, 1, 0, 0, 1, 0, 0]], device='cuda', dtype=torch.float32)
292
- b = torch.tensor([[0, 0, 1, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
293
- result, _ = circuits.sub_8bit(a, b)
294
- val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
295
- print(f"100 - 37 = {val} (expected 63)")
296
-
297
- a = torch.tensor([[0, 0, 0, 0, 1, 1, 0, 0]], device='cuda', dtype=torch.float32)
298
- b = torch.tensor([[0, 0, 0, 0, 1, 0, 1, 1]], device='cuda', dtype=torch.float32)
299
- result = circuits.mul_8bit(a, b)
300
- val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
301
- print(f"12 * 11 = {val} (expected 132)")
302
-
303
- a = torch.tensor([[0, 0, 1, 1, 0, 0, 1, 0]], device='cuda', dtype=torch.float32)
304
- b = torch.tensor([[0, 0, 0, 1, 1, 1, 1, 0]], device='cuda', dtype=torch.float32)
305
- gt = circuits.compare_gt(a, b)
306
- lt = circuits.compare_lt(a, b)
307
- eq = circuits.compare_eq(a, b)
308
- print(f"50 > 30: {int(gt[0].item())} (expected 1)")
309
- print(f"50 < 30: {int(lt[0].item())} (expected 0)")
310
- print(f"50 == 30: {int(eq[0].item())} (expected 0)")
311
-
312
- print("\nTesting batched forward...")
313
- batch_a = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
314
- batch_b = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
315
- op = torch.zeros(16, 6, device='cuda')
316
- op[:, 0] = 1.0
317
-
318
- result = circuits(batch_a, batch_b, op)
319
- print(f"Batch output shape: {result.shape}")
320
- print("Done.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
llm_integration/fitness.py DELETED
@@ -1,218 +0,0 @@
1
- """
2
- Shared fitness function for threshold circuit LLM integration.
3
- Randomized tests, no answer supervision - fitness IS the training signal.
4
- """
5
-
6
- import torch
7
- import random
8
- from typing import Callable, Dict, Tuple, List
9
-
10
- OPERATIONS = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
11
-
12
- def ground_truth(a: int, b: int, op: str) -> int:
13
- """Compute expected result (8-bit arithmetic)."""
14
- if op == 'add':
15
- return (a + b) & 0xFF
16
- elif op == 'sub':
17
- return (a - b) & 0xFF
18
- elif op == 'mul':
19
- return (a * b) & 0xFF
20
- elif op == 'gt':
21
- return 1 if a > b else 0
22
- elif op == 'lt':
23
- return 1 if a < b else 0
24
- elif op == 'eq':
25
- return 1 if a == b else 0
26
- else:
27
- raise ValueError(f"Unknown op: {op}")
28
-
29
-
30
- def int_to_bits(val: int, n_bits: int = 8) -> torch.Tensor:
31
- """Convert integer to bit tensor (MSB first)."""
32
- bits = torch.zeros(n_bits)
33
- for i in range(n_bits):
34
- bits[n_bits - 1 - i] = (val >> i) & 1
35
- return bits
36
-
37
-
38
- def bits_to_int(bits: torch.Tensor) -> int:
39
- """Convert bit tensor to integer (MSB first)."""
40
- val = 0
41
- n_bits = bits.shape[-1]
42
- for i in range(n_bits):
43
- val += int(bits[..., i].item()) << (n_bits - 1 - i)
44
- return val
45
-
46
-
47
- def op_to_idx(op: str) -> int:
48
- """Convert operation string to index."""
49
- return OPERATIONS.index(op)
50
-
51
-
52
- def idx_to_op(idx: int) -> str:
53
- """Convert index to operation string."""
54
- return OPERATIONS[idx]
55
-
56
-
57
- def generate_batch(batch_size: int, device: str = 'cuda') -> Dict[str, torch.Tensor]:
58
- """
59
- Generate a batch of random arithmetic problems.
60
-
61
- Returns:
62
- Dict with:
63
- 'a': [batch_size] int tensor of first operands
64
- 'b': [batch_size] int tensor of second operands
65
- 'op': [batch_size] int tensor of operation indices
66
- 'a_bits': [batch_size, 8] bit tensor
67
- 'b_bits': [batch_size, 8] bit tensor
68
- 'op_onehot': [batch_size, 6] one-hot operation tensor
69
- 'expected': [batch_size] int tensor of expected results
70
- 'expected_bits': [batch_size, 8] bit tensor of expected results
71
- """
72
- a_vals = torch.randint(0, 256, (batch_size,), device=device)
73
- b_vals = torch.randint(0, 256, (batch_size,), device=device)
74
- op_indices = torch.randint(0, len(OPERATIONS), (batch_size,), device=device)
75
-
76
- a_bits = torch.zeros(batch_size, 8, device=device)
77
- b_bits = torch.zeros(batch_size, 8, device=device)
78
- for i in range(8):
79
- a_bits[:, 7-i] = (a_vals >> i) & 1
80
- b_bits[:, 7-i] = (b_vals >> i) & 1
81
-
82
- op_onehot = torch.zeros(batch_size, len(OPERATIONS), device=device)
83
- op_onehot.scatter_(1, op_indices.unsqueeze(1), 1.0)
84
-
85
- expected = torch.zeros(batch_size, dtype=torch.long, device=device)
86
- for i in range(batch_size):
87
- a, b, op_idx = a_vals[i].item(), b_vals[i].item(), op_indices[i].item()
88
- expected[i] = ground_truth(a, b, idx_to_op(op_idx))
89
-
90
- expected_bits = torch.zeros(batch_size, 8, device=device)
91
- for i in range(8):
92
- expected_bits[:, 7-i] = (expected >> i) & 1
93
-
94
- return {
95
- 'a': a_vals,
96
- 'b': b_vals,
97
- 'op': op_indices,
98
- 'a_bits': a_bits.float(),
99
- 'b_bits': b_bits.float(),
100
- 'op_onehot': op_onehot.float(),
101
- 'expected': expected,
102
- 'expected_bits': expected_bits.float(),
103
- }
104
-
105
-
106
- def compute_fitness(
107
- model_fn: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor],
108
- n_samples: int = 10000,
109
- batch_size: int = 256,
110
- device: str = 'cuda',
111
- return_details: bool = False
112
- ) -> float | Tuple[float, Dict]:
113
- """
114
- Compute fitness score for a model.
115
-
116
- Args:
117
- model_fn: Function that takes (a_bits, b_bits, op_onehot) and returns result_bits
118
- n_samples: Number of test cases
119
- batch_size: Batch size for evaluation
120
- device: Device to run on
121
- return_details: If True, return per-operation breakdown
122
-
123
- Returns:
124
- Fitness score in [0, 1], optionally with details dict
125
- """
126
- correct = 0
127
- total = 0
128
- op_correct = {op: 0 for op in OPERATIONS}
129
- op_total = {op: 0 for op in OPERATIONS}
130
-
131
- for _ in range(0, n_samples, batch_size):
132
- actual_batch = min(batch_size, n_samples - total)
133
- batch = generate_batch(actual_batch, device)
134
-
135
- with torch.no_grad():
136
- pred_bits = model_fn(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
137
-
138
- pred_bits_binary = (pred_bits > 0.5).float()
139
-
140
- for i in range(actual_batch):
141
- pred_val = 0
142
- for j in range(8):
143
- pred_val += int(pred_bits_binary[i, j].item()) << (7 - j)
144
-
145
- expected_val = batch['expected'][i].item()
146
- op_name = idx_to_op(batch['op'][i].item())
147
-
148
- op_total[op_name] += 1
149
- total += 1
150
-
151
- if pred_val == expected_val:
152
- correct += 1
153
- op_correct[op_name] += 1
154
-
155
- fitness = correct / total if total > 0 else 0.0
156
-
157
- if return_details:
158
- details = {
159
- 'correct': correct,
160
- 'total': total,
161
- 'by_op': {
162
- op: {
163
- 'correct': op_correct[op],
164
- 'total': op_total[op],
165
- 'accuracy': op_correct[op] / op_total[op] if op_total[op] > 0 else 0.0
166
- }
167
- for op in OPERATIONS
168
- }
169
- }
170
- return fitness, details
171
-
172
- return fitness
173
-
174
-
175
- def compute_bit_accuracy(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> float:
176
- """Compute per-bit accuracy (for gradient signal analysis)."""
177
- pred_binary = (pred_bits > 0.5).float()
178
- return (pred_binary == expected_bits).float().mean().item()
179
-
180
-
181
- def compute_loss(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> torch.Tensor:
182
- """Binary cross-entropy loss on output bits."""
183
- pred_clamped = pred_bits.clamp(1e-7, 1 - 1e-7)
184
- return -((expected_bits * torch.log(pred_clamped) +
185
- (1 - expected_bits) * torch.log(1 - pred_clamped))).mean()
186
-
187
-
188
- if __name__ == "__main__":
189
- print("Testing fitness module...")
190
-
191
- batch = generate_batch(8, 'cpu')
192
- print(f"\nSample batch:")
193
- for i in range(4):
194
- a, b = batch['a'][i].item(), batch['b'][i].item()
195
- op = idx_to_op(batch['op'][i].item())
196
- expected = batch['expected'][i].item()
197
- print(f" {a} {op} {b} = {expected}")
198
-
199
- def random_model(a_bits, b_bits, op_onehot):
200
- return torch.rand(a_bits.shape[0], 8, device=a_bits.device)
201
-
202
- fitness = compute_fitness(random_model, n_samples=1000, batch_size=100, device='cpu')
203
- print(f"\nRandom model fitness: {fitness:.4f} (expected ~0.004 for 8-bit)")
204
-
205
- def perfect_model(a_bits, b_bits, op_onehot):
206
- batch_size = a_bits.shape[0]
207
- results = torch.zeros(batch_size, 8, device=a_bits.device)
208
- for i in range(batch_size):
209
- a = sum(int(a_bits[i, j].item()) << (7-j) for j in range(8))
210
- b = sum(int(b_bits[i, j].item()) << (7-j) for j in range(8))
211
- op_idx = op_onehot[i].argmax().item()
212
- result = ground_truth(a, b, idx_to_op(op_idx))
213
- for j in range(8):
214
- results[i, 7-j] = (result >> j) & 1
215
- return results
216
-
217
- fitness = compute_fitness(perfect_model, n_samples=1000, batch_size=100, device='cpu')
218
- print(f"Perfect model fitness: {fitness:.4f} (expected 1.0)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
llm_integration/{model.py → llm.py} RENAMED
@@ -1,24 +1,402 @@
1
- """
2
- Trainable interface layers for frozen threshold circuits.
3
- BitEncoder, OpRouter, BitDecoder wrap the frozen circuits.
4
- HiddenStateExtractor and AugmentedArithmeticModel for LLM integration.
5
- """
 
 
 
6
 
7
  import torch
8
  import torch.nn as nn
9
  import torch.nn.functional as F
10
- from circuits import FrozenThresholdCircuits, heaviside_ste
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- MODEL_ID = 'HuggingFaceTB/SmolLM2-360M-Instruct'
13
  OPERATIONS = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  OP_SYMBOLS = {'add': '+', 'sub': '-', 'mul': '*', 'gt': '>', 'lt': '<', 'eq': '=='}
15
 
16
 
17
  class BitEncoder(nn.Module):
18
- """
19
- Encodes two 8-bit operands from input representation.
20
- Uses residual connection to preserve ground truth bits while allowing learned refinement.
21
- """
22
 
23
  def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32):
24
  super().__init__()
@@ -30,14 +408,7 @@ class BitEncoder(nn.Module):
30
  self.scale = nn.Parameter(torch.tensor(0.0))
31
 
32
  def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
33
- """
34
- Args:
35
- x: [batch, input_dim] input with first 16 dims being a_bits, b_bits
36
-
37
- Returns:
38
- a_bits: [batch, 8] first operand bits
39
- b_bits: [batch, 8] second operand bits
40
- """
41
  base_bits = x[:, :16]
42
  refinement = self.refine(x) * torch.sigmoid(self.scale)
43
  bits = base_bits + refinement
@@ -49,10 +420,7 @@ class BitEncoder(nn.Module):
49
 
50
 
51
  class OpRouter(nn.Module):
52
- """
53
- Routes computation to the appropriate circuit based on input.
54
- Outputs soft weights over operations for gradient flow.
55
- """
56
 
57
  def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32, n_ops: int = 6):
58
  super().__init__()
@@ -63,23 +431,13 @@ class OpRouter(nn.Module):
63
  )
64
 
65
  def forward(self, x: torch.Tensor) -> torch.Tensor:
66
- """
67
- Args:
68
- x: [batch, input_dim] input features
69
-
70
- Returns:
71
- op_weights: [batch, n_ops] soft operation weights (softmax)
72
- """
73
  logits = self.net(x)
74
  return F.softmax(logits, dim=-1)
75
 
76
 
77
  class BitDecoder(nn.Module):
78
- """
79
- Decodes circuit output bits to target representation.
80
- For standalone training: outputs soft bits for loss computation.
81
- For LLM integration: would project to hidden state delta.
82
- """
83
 
84
  def __init__(self, output_dim: int = 8):
85
  super().__init__()
@@ -90,10 +448,7 @@ class BitDecoder(nn.Module):
90
 
91
 
92
  class ThresholdALU(nn.Module):
93
- """
94
- Complete trainable interface + frozen circuits.
95
- Learns to encode inputs, route to circuits, decode outputs.
96
- """
97
 
98
  def __init__(self, device: str = 'cuda'):
99
  super().__init__()
@@ -110,17 +465,7 @@ class ThresholdALU(nn.Module):
110
 
111
  def forward(self, a_bits_in: torch.Tensor, b_bits_in: torch.Tensor,
112
  op_onehot: torch.Tensor) -> torch.Tensor:
113
- """
114
- Forward pass through trainable interface + frozen circuits.
115
-
116
- Args:
117
- a_bits_in: [batch, 8] input A bits (ground truth for training)
118
- b_bits_in: [batch, 8] input B bits (ground truth for training)
119
- op_onehot: [batch, 6] one-hot operation selector
120
-
121
- Returns:
122
- result_bits: [batch, 8] output bits
123
- """
124
  x = torch.cat([a_bits_in, b_bits_in, op_onehot], dim=-1)
125
 
126
  a_bits, b_bits = self.encoder(x)
@@ -142,10 +487,7 @@ class ThresholdALU(nn.Module):
142
 
143
 
144
  class DirectCircuitModel(nn.Module):
145
- """
146
- Minimal model that directly uses circuits without learned encoding.
147
- For validating that circuits themselves achieve 100% fitness.
148
- """
149
 
150
  def __init__(self, device: str = 'cuda'):
151
  super().__init__()
@@ -158,10 +500,7 @@ class DirectCircuitModel(nn.Module):
158
 
159
 
160
  class HiddenStateExtractor(nn.Module):
161
- """
162
- Extracts operands and operation from LLM hidden states.
163
- This is the hard part - must learn to parse numbers from embeddings.
164
- """
165
 
166
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256):
167
  super().__init__()
@@ -185,15 +524,7 @@ class HiddenStateExtractor(nn.Module):
185
  )
186
 
187
  def forward(self, hidden_states: torch.Tensor):
188
- """
189
- Args:
190
- hidden_states: [batch, hidden_dim] from LLM
191
-
192
- Returns:
193
- a_bits: [batch, 8]
194
- b_bits: [batch, 8]
195
- op_logits: [batch, 6]
196
- """
197
  a_logits = self.a_extractor(hidden_states)
198
  b_logits = self.b_extractor(hidden_states)
199
  op_logits = self.op_router(hidden_states)
@@ -211,10 +542,7 @@ class HiddenStateExtractor(nn.Module):
211
 
212
 
213
  class AttentionPooling(nn.Module):
214
- """
215
- Learnable attention pooling over sequence positions.
216
- Replaces mean pooling - learns which tokens matter for extraction.
217
- """
218
 
219
  def __init__(self, hidden_dim: int = 960, num_heads: int = 4):
220
  super().__init__()
@@ -229,14 +557,7 @@ class AttentionPooling(nn.Module):
229
  self.cls_token = nn.Parameter(torch.randn(1, 1, hidden_dim) * 0.02)
230
 
231
  def forward(self, embeddings: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
232
- """
233
- Args:
234
- embeddings: [batch, seq_len, hidden_dim]
235
- mask: [batch, seq_len] attention mask (1 = attend, 0 = ignore)
236
-
237
- Returns:
238
- pooled: [batch, hidden_dim]
239
- """
240
  batch_size, seq_len, hidden_dim = embeddings.shape
241
 
242
  cls_expanded = self.cls_token.expand(batch_size, -1, -1)
@@ -271,10 +592,7 @@ class AttentionPooling(nn.Module):
271
 
272
 
273
  class MultiHeadBitExtractor(nn.Module):
274
- """
275
- 8 separate extractors for 8 bits - each bit gets its own specialized network.
276
- More expressive than single MLP predicting all 8 bits at once.
277
- """
278
 
279
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 128):
280
  super().__init__()
@@ -289,13 +607,7 @@ class MultiHeadBitExtractor(nn.Module):
289
  ])
290
 
291
  def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
292
- """
293
- Args:
294
- hidden_states: [batch, hidden_dim]
295
-
296
- Returns:
297
- bits: [batch, 8] - one bit from each extractor
298
- """
299
  hidden_states = torch.nan_to_num(hidden_states, nan=0.0)
300
 
301
  bit_logits = [extractor(hidden_states) for extractor in self.bit_extractors]
@@ -310,10 +622,7 @@ class MultiHeadBitExtractor(nn.Module):
310
 
311
 
312
  class Extractor(nn.Module):
313
- """
314
- Extracts operands and operation from LLM hidden states.
315
- Uses attention pooling and per-bit extraction networks.
316
- """
317
 
318
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
319
  super().__init__()
@@ -330,16 +639,7 @@ class Extractor(nn.Module):
330
  )
331
 
332
  def forward(self, embeddings: torch.Tensor, mask: torch.Tensor):
333
- """
334
- Args:
335
- embeddings: [batch, seq_len, hidden_dim]
336
- mask: [batch, seq_len]
337
-
338
- Returns:
339
- a_bits: [batch, 8]
340
- b_bits: [batch, 8]
341
- op_logits: [batch, 6]
342
- """
343
  pooled = self.attention_pool(embeddings, mask)
344
 
345
  a_bits, _ = self.a_extractor(pooled)
@@ -350,22 +650,7 @@ class Extractor(nn.Module):
350
 
351
 
352
  class PositionExtractor(nn.Module):
353
- """
354
- Position-specific extraction with dynamic operator detection.
355
-
356
- Tokenization pattern for "A op B":
357
- [A_digits...] [operator] [space] [B_digits...]
358
-
359
- Examples:
360
- "5 + 3" -> ['5', ' +', ' ', '3'] (positions: A=0, op=1, B=3)
361
- "47 + 86" -> ['4', '7', ' +', ' ', '8', '6'] (positions: A=0-1, op=2, B=4-5)
362
- "127 + 128" -> ['1','2','7',' +', ' ','1','2','8'] (positions: A=0-2, op=3, B=5-7)
363
-
364
- Token IDs (SmolLM2):
365
- Digits '0'-'9': 32-41
366
- Operators: ' +'=1232, ' -'=731, ' *'=1672, ' >'=2986, ' <'=2067, ' =='=1758
367
- Space: 216
368
- """
369
 
370
  DIGIT_TOKENS = set(range(32, 42))
371
  OPERATOR_TOKENS = {
@@ -406,33 +691,14 @@ class PositionExtractor(nn.Module):
406
  )
407
 
408
  def _find_operator_position(self, token_ids: torch.Tensor) -> tuple[int, int]:
409
- """
410
- Find operator token position and its operation index.
411
-
412
- Args:
413
- token_ids: [seq_len] tensor of token IDs
414
-
415
- Returns:
416
- (position, op_index) or (-1, -1) if not found
417
- """
418
  for pos, tid in enumerate(token_ids.tolist()):
419
  if tid in self.OPERATOR_TOKENS:
420
  return pos, self.OPERATOR_TOKENS[tid]
421
  return -1, -1
422
 
423
  def _extract_digit_features(self, hidden: torch.Tensor, start: int, end: int) -> torch.Tensor:
424
- """
425
- Extract and pad digit hidden states to fixed size.
426
-
427
- Args:
428
- hidden: [seq_len, hidden_dim]
429
- start: start position (inclusive)
430
- end: end position (exclusive)
431
-
432
- Returns:
433
- [hidden_dim * MAX_DIGITS] flattened features, zero-padded on the LEFT
434
- (so units digit is always at the same position regardless of number length)
435
- """
436
  n_digits = end - start
437
  features = torch.zeros(self.MAX_DIGITS * self.hidden_dim, device=hidden.device)
438
 
@@ -444,17 +710,7 @@ class PositionExtractor(nn.Module):
444
  return features
445
 
446
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
447
- """
448
- Args:
449
- hidden: [batch, seq_len, hidden_dim]
450
- mask: [batch, seq_len] attention mask
451
- token_ids: [batch, seq_len] token IDs (required for operator detection)
452
-
453
- Returns:
454
- a_bits: [batch, 8]
455
- b_bits: [batch, 8]
456
- op_logits: [batch, 6]
457
- """
458
  if token_ids is None:
459
  raise ValueError("PositionExtractor requires token_ids for operator detection")
460
 
@@ -515,19 +771,7 @@ class PositionExtractor(nn.Module):
515
 
516
 
517
  class PositionalDigitExtractor(nn.Module):
518
- """
519
- Position-aware digit extraction: classifies each digit position independently.
520
-
521
- This approach achieves 100% accuracy because:
522
- 1. Each digit token position is classified independently (100% accuracy on layer 0)
523
- 2. Numbers are reconstructed using place values (×100, ×10, ×1)
524
- 3. No information is lost through pooling
525
-
526
- Token IDs (SmolLM2):
527
- Digits '0'-'9': 32-41
528
- Operators: ' +'=1232, ' -'=731, ' *'=1672, ' >'=2986, ' <'=2067, ' =='=1758
529
- Space: 216
530
- """
531
 
532
  DIGIT_TOKENS = set(range(32, 42))
533
  OPERATOR_TOKENS = {
@@ -605,20 +849,7 @@ class PositionalDigitExtractor(nn.Module):
605
  return torch.stack(bits)
606
 
607
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor):
608
- """
609
- Args:
610
- hidden: [batch, seq_len, hidden_dim]
611
- mask: [batch, seq_len]
612
- token_ids: [batch, seq_len]
613
-
614
- Returns:
615
- a_bits: [batch, 8]
616
- b_bits: [batch, 8]
617
- op_logits: [batch, 6]
618
- op_indices: [batch] ground truth op from tokens
619
- a_digit_logits: list of [batch, 10] per digit position
620
- b_digit_logits: list of [batch, 10] per digit position
621
- """
622
  batch_size = hidden.shape[0]
623
  device = hidden.device
624
 
@@ -672,10 +903,7 @@ class PositionalDigitExtractor(nn.Module):
672
 
673
 
674
  class DigitExtractor(nn.Module):
675
- """
676
- Digit-level extraction: predicts digits (0-9) then converts to bits.
677
- Uses attention pooling (less accurate than PositionalDigitExtractor).
678
- """
679
 
680
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
681
  super().__init__()
@@ -701,11 +929,7 @@ class DigitExtractor(nn.Module):
701
  )
702
 
703
  def digits_to_bits(self, digit_logits: torch.Tensor) -> torch.Tensor:
704
- """
705
- Convert 3-digit predictions to 8-bit representation.
706
- digit_logits: [batch, 30] (3 digits * 10 classes each)
707
- Returns: [batch, 8] bits
708
- """
709
  batch_size = digit_logits.shape[0]
710
 
711
  logits = digit_logits.view(batch_size, 3, 10)
@@ -729,10 +953,7 @@ class DigitExtractor(nn.Module):
729
  return torch.stack(bits, dim=-1)
730
 
731
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor):
732
- """
733
- Returns:
734
- a_bits, b_bits, op_logits, a_digit_logits, b_digit_logits
735
- """
736
  pooled = self.attention_pool(hidden, mask)
737
 
738
  a_digit_logits = self.a_digit_pred(pooled)
@@ -746,15 +967,7 @@ class DigitExtractor(nn.Module):
746
 
747
 
748
  class HybridExtractor(nn.Module):
749
- """
750
- Hybrid extractor that handles both digit tokens and word numbers.
751
-
752
- For digit tokens (32-41): Direct lookup, no training needed
753
- For word numbers: Learned MLP extraction from pooled hidden states
754
-
755
- This is the real training target - learning to extract numbers from
756
- natural language like "forty seven plus eighty six".
757
- """
758
 
759
  DIGIT_TOKENS = set(range(32, 42))
760
  SYMBOL_OP_TOKENS = {
@@ -820,11 +1033,7 @@ class HybridExtractor(nn.Module):
820
  return -1
821
 
822
  def _extract_from_digits(self, token_ids: torch.Tensor) -> tuple:
823
- """
824
- Extract values directly from digit tokens (hardcoded lookup).
825
- Handles both symbol operators (' +') and word operators ('plus').
826
- Returns (a_value, b_value, op_idx) or None if pattern not found.
827
- """
828
  tokens = token_ids.tolist()
829
 
830
  op_pos = -1
@@ -873,11 +1082,7 @@ class HybridExtractor(nn.Module):
873
  return bits
874
 
875
  def _digits_to_value_and_bits(self, digit_logits: torch.Tensor, device) -> tuple:
876
- """
877
- Convert 3-digit logits to value and bits.
878
- digit_logits: [30] (3 digits × 10 classes)
879
- Returns: (value tensor, bits tensor [8])
880
- """
881
  logits = digit_logits.view(3, 10)
882
  probs = torch.softmax(logits, dim=-1)
883
 
@@ -900,16 +1105,7 @@ class HybridExtractor(nn.Module):
900
  return hundreds * 100 + tens * 10 + ones, torch.stack(bits)
901
 
902
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
903
- """
904
- Args:
905
- hidden: [batch, seq_len, hidden_dim]
906
- mask: [batch, seq_len]
907
- token_ids: [batch, seq_len] - optional, enables digit lookup
908
-
909
- Returns:
910
- a_bits, b_bits, op_logits, a_values, b_values, used_lookup,
911
- a_digit_logits, b_digit_logits
912
- """
913
  batch_size = hidden.shape[0]
914
  device = hidden.device
915
 
@@ -1019,10 +1215,7 @@ class HybridExtractor(nn.Module):
1019
 
1020
 
1021
  class ArithmeticModel(nn.Module):
1022
- """
1023
- LLM + extractor + frozen threshold circuits.
1024
- Optionally unfreeze top N transformer layers with --unfreeze_layers.
1025
- """
1026
 
1027
  def __init__(self, device: str = 'cuda', unfreeze_layers: int = 0,
1028
  extract_layer: int = -1, position_extract: bool = False,
@@ -1117,14 +1310,7 @@ class ArithmeticModel(nn.Module):
1117
  print(f" Total trainable: {total_trainable:,}", flush=True)
1118
 
1119
  def get_hidden_states(self, texts: list[str]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
1120
- """
1121
- Get hidden states from specified layer.
1122
-
1123
- Returns:
1124
- hidden: [batch, seq_len, hidden_dim] hidden states
1125
- mask: [batch, seq_len] attention mask
1126
- token_ids: [batch, seq_len] input token IDs
1127
- """
1128
  inputs = self.tokenizer(
1129
  texts,
1130
  return_tensors='pt',
@@ -1146,15 +1332,7 @@ class ArithmeticModel(nn.Module):
1146
  return hidden, mask, token_ids
1147
 
1148
  def forward(self, texts: list[str]):
1149
- """
1150
- Full forward pass: text -> hidden states -> extractor -> circuits -> result
1151
-
1152
- Returns:
1153
- result_bits, a_bits, b_bits, op_logits
1154
- If digit_pred: also returns a_digit_logits, b_digit_logits
1155
- If position_extract or positional_digit: also returns op_indices
1156
- If positional_digit: also returns a_values, b_values
1157
- """
1158
  hidden, mask, token_ids = self.get_hidden_states(texts)
1159
 
1160
  if self.hybrid or self.positional_digit or self.position_extract:
@@ -1207,10 +1385,1182 @@ class ArithmeticModel(nn.Module):
1207
  return params
1208
 
1209
 
1210
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
  import sys
1212
  sys.path.insert(0, '.')
1213
- from fitness import generate_batch, compute_fitness, OPERATIONS
1214
 
1215
  print("Testing model components...")
1216
 
@@ -1261,3 +2611,14 @@ if __name__ == "__main__":
1261
  print(f" Total trainable: {total:,}")
1262
 
1263
  print("\nDone.")
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Frozen threshold circuits inside SmolLM2: the circuits, the fitness, the interface, training, baseline."""
2
+ import argparse
3
+ import os
4
+ import random
5
+ import re
6
+ import sys
7
+ import time
8
+ from typing import Dict, Tuple, Callable, List
9
 
10
  import torch
11
  import torch.nn as nn
12
  import torch.nn.functional as F
13
+ import torch.optim as optim
14
+ from safetensors import safe_open
15
+ from transformers import AutoModelForCausalLM, AutoTokenizer
16
+
17
+ TRAINED = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'trained')
18
+
19
+
20
+ MODEL_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'neural_computer.safetensors')
21
+
22
+
23
+ def heaviside(x: torch.Tensor) -> torch.Tensor:
24
+ """Standard Heaviside step function."""
25
+ return (x >= 0).float()
26
+
27
+
28
+ class HeavisideSTE(torch.autograd.Function):
29
+ """Heaviside with straight-through estimator for gradients."""
30
+ @staticmethod
31
+ def forward(ctx, x):
32
+ return (x >= 0).float()
33
+
34
+ @staticmethod
35
+ def backward(ctx, grad_output):
36
+ return grad_output
37
+
38
+
39
+ def heaviside_ste(x: torch.Tensor) -> torch.Tensor:
40
+ """Heaviside with STE gradient."""
41
+ return HeavisideSTE.apply(x)
42
+
43
+
44
+ class FrozenThresholdCircuits(nn.Module):
45
+ """Wrapper for frozen threshold logic circuits."""
46
+
47
+ def __init__(self, model_path: str = MODEL_PATH, device: str = 'cuda'):
48
+ super().__init__()
49
+ self.device = device
50
+ self.weights = {}
51
+ self._load_weights(model_path)
52
+
53
+ def _load_weights(self, path: str):
54
+ """Load weights from safetensors file."""
55
+ with safe_open(path, framework='pt') as f:
56
+ for name in f.keys():
57
+ tensor = f.get_tensor(name).to(self.device).float()
58
+ self.weights[name] = tensor
59
+
60
+ def _gate(self, inputs: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor) -> torch.Tensor:
61
+ """Execute single threshold gate with STE."""
62
+ weight = weight.view(-1)
63
+ bias = bias.view(-1)
64
+ pre_activation = (inputs * weight).sum(dim=-1) + bias
65
+ return heaviside_ste(pre_activation)
66
+
67
+ def _xor(self, a: torch.Tensor, b: torch.Tensor, prefix: str) -> torch.Tensor:
68
+ """XOR via OR-NAND-AND pattern (2 layers)."""
69
+ inputs = torch.stack([a, b], dim=-1)
70
+
71
+ w_or = self.weights[f'{prefix}.layer1.or.weight']
72
+ b_or = self.weights[f'{prefix}.layer1.or.bias']
73
+ w_nand = self.weights[f'{prefix}.layer1.nand.weight']
74
+ b_nand = self.weights[f'{prefix}.layer1.nand.bias']
75
+
76
+ h_or = self._gate(inputs, w_or, b_or)
77
+ h_nand = self._gate(inputs, w_nand, b_nand)
78
+
79
+ hidden = torch.stack([h_or, h_nand], dim=-1)
80
+ w2 = self.weights[f'{prefix}.layer2.weight']
81
+ b2 = self.weights[f'{prefix}.layer2.bias']
82
+
83
+ return self._gate(hidden, w2, b2)
84
+
85
+ def _full_adder(self, a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor,
86
+ prefix: str) -> Tuple[torch.Tensor, torch.Tensor]:
87
+ """Full adder: sum and carry out."""
88
+ ha1_sum = self._xor(a, b, f'{prefix}.ha1.sum')
89
+
90
+ inp_carry1 = torch.stack([a, b], dim=-1)
91
+ w_c1 = self.weights[f'{prefix}.ha1.carry.weight']
92
+ b_c1 = self.weights[f'{prefix}.ha1.carry.bias']
93
+ ha1_carry = self._gate(inp_carry1, w_c1, b_c1)
94
+
95
+ ha2_sum = self._xor(ha1_sum, cin, f'{prefix}.ha2.sum')
96
+
97
+ inp_carry2 = torch.stack([ha1_sum, cin], dim=-1)
98
+ w_c2 = self.weights[f'{prefix}.ha2.carry.weight']
99
+ b_c2 = self.weights[f'{prefix}.ha2.carry.bias']
100
+ ha2_carry = self._gate(inp_carry2, w_c2, b_c2)
101
+
102
+ inp_cout = torch.stack([ha1_carry, ha2_carry], dim=-1)
103
+ w_cout = self.weights[f'{prefix}.carry_or.weight']
104
+ b_cout = self.weights[f'{prefix}.carry_or.bias']
105
+ cout = self._gate(inp_cout, w_cout, b_cout)
106
+
107
+ return ha2_sum, cout
108
+
109
+ def add_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
110
+ """8-bit ripple carry addition."""
111
+ batch_size = a_bits.shape[0]
112
+ carry = torch.zeros(batch_size, device=self.device)
113
+ result_bits = []
114
+
115
+ for bit in range(8):
116
+ bit_idx = 7 - bit
117
+ s, carry = self._full_adder(
118
+ a_bits[:, bit_idx],
119
+ b_bits[:, bit_idx],
120
+ carry,
121
+ f'arithmetic.ripplecarry8bit.fa{bit}'
122
+ )
123
+ result_bits.insert(0, s)
124
+
125
+ result = torch.stack(result_bits, dim=1)
126
+ return result, carry
127
+
128
+ def sub_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
129
+ """8-bit subtraction by two's complement, A - B = A + ~B + 1."""
130
+ b_inv = 1.0 - b_bits
131
+ batch_size = a_bits.shape[0]
132
+ carry = torch.ones(batch_size, device=self.device)
133
+ result_bits = []
134
+
135
+ for bit in range(8):
136
+ bit_idx = 7 - bit
137
+ s, carry = self._full_adder(
138
+ a_bits[:, bit_idx],
139
+ b_inv[:, bit_idx],
140
+ carry,
141
+ f'arithmetic.ripplecarry8bit.fa{bit}'
142
+ )
143
+ result_bits.insert(0, s)
144
+
145
+ result = torch.stack(result_bits, dim=1)
146
+ borrow = 1.0 - carry
147
+ return result, borrow
148
+
149
+ def mul_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
150
+ """8-bit multiplication via shift-add (software implementation using adder circuits)."""
151
+ batch_size = a_bits.shape[0]
152
+
153
+ acc = torch.zeros(batch_size, 8, device=self.device)
154
+
155
+ for i in range(8):
156
+ b_bit = b_bits[:, 7 - i]
157
+ pp = a_bits * b_bit.unsqueeze(1)
158
+
159
+ shifted_pp = torch.zeros(batch_size, 8, device=self.device)
160
+ for j in range(8):
161
+ dst_idx = j + i
162
+ if dst_idx < 8:
163
+ shifted_pp[:, 7 - dst_idx] = pp[:, 7 - j]
164
+
165
+ acc, _ = self.add_8bit(acc, shifted_pp)
166
+
167
+ return acc
168
+
169
+ def compare_gt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
170
+ """A > B comparison."""
171
+ inputs = torch.cat([a_bits, b_bits], dim=-1)
172
+ w = self.weights['arithmetic.greaterthan8bit.weight'].view(-1)
173
+ b = self.weights['arithmetic.greaterthan8bit.bias'].view(-1)
174
+ return heaviside_ste((inputs * w).sum(dim=-1) + b)
175
+
176
+ def compare_lt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
177
+ """A < B comparison."""
178
+ inputs = torch.cat([a_bits, b_bits], dim=-1)
179
+ w = self.weights['arithmetic.lessthan8bit.weight'].view(-1)
180
+ b = self.weights['arithmetic.lessthan8bit.bias'].view(-1)
181
+ return heaviside_ste((inputs * w).sum(dim=-1) + b)
182
+
183
+ def compare_eq(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
184
+ """A == B comparison (two-layer)."""
185
+ inputs = torch.cat([a_bits, b_bits], dim=-1)
186
+ prefix = 'arithmetic.equality8bit'
187
+
188
+ w_geq = self.weights[f'{prefix}.layer1.geq.weight'].view(-1)
189
+ b_geq = self.weights[f'{prefix}.layer1.geq.bias'].view(-1)
190
+ w_leq = self.weights[f'{prefix}.layer1.leq.weight'].view(-1)
191
+ b_leq = self.weights[f'{prefix}.layer1.leq.bias'].view(-1)
192
+
193
+ h_geq = heaviside_ste((inputs * w_geq).sum(dim=-1) + b_geq)
194
+ h_leq = heaviside_ste((inputs * w_leq).sum(dim=-1) + b_leq)
195
+
196
+ hidden = torch.stack([h_geq, h_leq], dim=-1)
197
+ w2 = self.weights[f'{prefix}.layer2.weight'].view(-1)
198
+ b2 = self.weights[f'{prefix}.layer2.bias'].view(-1)
199
+
200
+ return heaviside_ste((hidden * w2).sum(dim=-1) + b2)
201
+
202
+ def forward(self, a_bits: torch.Tensor, b_bits: torch.Tensor,
203
+ op_onehot: torch.Tensor) -> torch.Tensor:
204
+ """Execute operation based on one-hot selector."""
205
+ batch_size = a_bits.shape[0]
206
+
207
+ add_result, _ = self.add_8bit(a_bits, b_bits)
208
+ sub_result, _ = self.sub_8bit(a_bits, b_bits)
209
+ mul_result = self.mul_8bit(a_bits, b_bits)
210
+
211
+ gt_result = self.compare_gt(a_bits, b_bits)
212
+ lt_result = self.compare_lt(a_bits, b_bits)
213
+ eq_result = self.compare_eq(a_bits, b_bits)
214
+
215
+ cmp_expanded = torch.zeros(batch_size, 8, device=self.device)
216
+
217
+ gt_expanded = cmp_expanded.clone()
218
+ gt_expanded[:, 7] = gt_result
219
+
220
+ lt_expanded = cmp_expanded.clone()
221
+ lt_expanded[:, 7] = lt_result
222
+
223
+ eq_expanded = cmp_expanded.clone()
224
+ eq_expanded[:, 7] = eq_result
225
+
226
+ results = torch.stack([
227
+ add_result,
228
+ sub_result,
229
+ mul_result,
230
+ gt_expanded,
231
+ lt_expanded,
232
+ eq_expanded
233
+ ], dim=1)
234
+
235
+ op_weights = op_onehot.unsqueeze(-1)
236
+ output = (results * op_weights).sum(dim=1)
237
+
238
+ return output
239
 
 
240
  OPERATIONS = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
241
+
242
+ def ground_truth(a: int, b: int, op: str) -> int:
243
+ """Compute expected result (8-bit arithmetic)."""
244
+ if op == 'add':
245
+ return (a + b) & 0xFF
246
+ elif op == 'sub':
247
+ return (a - b) & 0xFF
248
+ elif op == 'mul':
249
+ return (a * b) & 0xFF
250
+ elif op == 'gt':
251
+ return 1 if a > b else 0
252
+ elif op == 'lt':
253
+ return 1 if a < b else 0
254
+ elif op == 'eq':
255
+ return 1 if a == b else 0
256
+ else:
257
+ raise ValueError(f"Unknown op: {op}")
258
+
259
+
260
+ def int_to_bits(val: int, n_bits: int = 8) -> torch.Tensor:
261
+ """Convert integer to bit tensor (MSB first)."""
262
+ bits = torch.zeros(n_bits)
263
+ for i in range(n_bits):
264
+ bits[n_bits - 1 - i] = (val >> i) & 1
265
+ return bits
266
+
267
+
268
+ def bits_to_int(bits: torch.Tensor) -> int:
269
+ """Convert bit tensor to integer (MSB first)."""
270
+ val = 0
271
+ n_bits = bits.shape[-1]
272
+ for i in range(n_bits):
273
+ val += int(bits[..., i].item()) << (n_bits - 1 - i)
274
+ return val
275
+
276
+
277
+ def op_to_idx(op: str) -> int:
278
+ """Convert operation string to index."""
279
+ return OPERATIONS.index(op)
280
+
281
+
282
+ def idx_to_op(idx: int) -> str:
283
+ """Convert index to operation string."""
284
+ return OPERATIONS[idx]
285
+
286
+
287
+ def generate_batch(batch_size: int, device: str = 'cuda') -> Dict[str, torch.Tensor]:
288
+ """Generate a batch of random arithmetic problems."""
289
+ a_vals = torch.randint(0, 256, (batch_size,), device=device)
290
+ b_vals = torch.randint(0, 256, (batch_size,), device=device)
291
+ op_indices = torch.randint(0, len(OPERATIONS), (batch_size,), device=device)
292
+
293
+ a_bits = torch.zeros(batch_size, 8, device=device)
294
+ b_bits = torch.zeros(batch_size, 8, device=device)
295
+ for i in range(8):
296
+ a_bits[:, 7-i] = (a_vals >> i) & 1
297
+ b_bits[:, 7-i] = (b_vals >> i) & 1
298
+
299
+ op_onehot = torch.zeros(batch_size, len(OPERATIONS), device=device)
300
+ op_onehot.scatter_(1, op_indices.unsqueeze(1), 1.0)
301
+
302
+ expected = torch.zeros(batch_size, dtype=torch.long, device=device)
303
+ for i in range(batch_size):
304
+ a, b, op_idx = a_vals[i].item(), b_vals[i].item(), op_indices[i].item()
305
+ expected[i] = ground_truth(a, b, idx_to_op(op_idx))
306
+
307
+ expected_bits = torch.zeros(batch_size, 8, device=device)
308
+ for i in range(8):
309
+ expected_bits[:, 7-i] = (expected >> i) & 1
310
+
311
+ return {
312
+ 'a': a_vals,
313
+ 'b': b_vals,
314
+ 'op': op_indices,
315
+ 'a_bits': a_bits.float(),
316
+ 'b_bits': b_bits.float(),
317
+ 'op_onehot': op_onehot.float(),
318
+ 'expected': expected,
319
+ 'expected_bits': expected_bits.float(),
320
+ }
321
+
322
+
323
+ def compute_fitness(
324
+ model_fn: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor],
325
+ n_samples: int = 10000,
326
+ batch_size: int = 256,
327
+ device: str = 'cuda',
328
+ return_details: bool = False
329
+ ) -> float | Tuple[float, Dict]:
330
+ """Compute fitness score for a model."""
331
+ correct = 0
332
+ total = 0
333
+ op_correct = {op: 0 for op in OPERATIONS}
334
+ op_total = {op: 0 for op in OPERATIONS}
335
+
336
+ for _ in range(0, n_samples, batch_size):
337
+ actual_batch = min(batch_size, n_samples - total)
338
+ batch = generate_batch(actual_batch, device)
339
+
340
+ with torch.no_grad():
341
+ pred_bits = model_fn(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
342
+
343
+ pred_bits_binary = (pred_bits > 0.5).float()
344
+
345
+ for i in range(actual_batch):
346
+ pred_val = 0
347
+ for j in range(8):
348
+ pred_val += int(pred_bits_binary[i, j].item()) << (7 - j)
349
+
350
+ expected_val = batch['expected'][i].item()
351
+ op_name = idx_to_op(batch['op'][i].item())
352
+
353
+ op_total[op_name] += 1
354
+ total += 1
355
+
356
+ if pred_val == expected_val:
357
+ correct += 1
358
+ op_correct[op_name] += 1
359
+
360
+ fitness = correct / total if total > 0 else 0.0
361
+
362
+ if return_details:
363
+ details = {
364
+ 'correct': correct,
365
+ 'total': total,
366
+ 'by_op': {
367
+ op: {
368
+ 'correct': op_correct[op],
369
+ 'total': op_total[op],
370
+ 'accuracy': op_correct[op] / op_total[op] if op_total[op] > 0 else 0.0
371
+ }
372
+ for op in OPERATIONS
373
+ }
374
+ }
375
+ return fitness, details
376
+
377
+ return fitness
378
+
379
+
380
+ def compute_bit_accuracy(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> float:
381
+ """Compute per-bit accuracy (for gradient signal analysis)."""
382
+ pred_binary = (pred_bits > 0.5).float()
383
+ return (pred_binary == expected_bits).float().mean().item()
384
+
385
+
386
+ def compute_loss(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> torch.Tensor:
387
+ """Binary cross-entropy loss on output bits."""
388
+ pred_clamped = pred_bits.clamp(1e-7, 1 - 1e-7)
389
+ return -((expected_bits * torch.log(pred_clamped) +
390
+ (1 - expected_bits) * torch.log(1 - pred_clamped))).mean()
391
+
392
+
393
+ MODEL_ID = 'HuggingFaceTB/SmolLM2-360M-Instruct'
394
+
395
  OP_SYMBOLS = {'add': '+', 'sub': '-', 'mul': '*', 'gt': '>', 'lt': '<', 'eq': '=='}
396
 
397
 
398
  class BitEncoder(nn.Module):
399
+ """Encodes two 8-bit operands from input representation."""
 
 
 
400
 
401
  def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32):
402
  super().__init__()
 
408
  self.scale = nn.Parameter(torch.tensor(0.0))
409
 
410
  def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
411
+ """Split x, whose first 16 features are the bits of a and b, into the two operands."""
 
 
 
 
 
 
 
412
  base_bits = x[:, :16]
413
  refinement = self.refine(x) * torch.sigmoid(self.scale)
414
  bits = base_bits + refinement
 
420
 
421
 
422
  class OpRouter(nn.Module):
423
+ """Routes computation to the appropriate circuit based on input."""
 
 
 
424
 
425
  def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32, n_ops: int = 6):
426
  super().__init__()
 
431
  )
432
 
433
  def forward(self, x: torch.Tensor) -> torch.Tensor:
434
+ """Route each input row to the circuit its features select."""
 
 
 
 
 
 
435
  logits = self.net(x)
436
  return F.softmax(logits, dim=-1)
437
 
438
 
439
  class BitDecoder(nn.Module):
440
+ """Decodes circuit output bits to target representation."""
 
 
 
 
441
 
442
  def __init__(self, output_dim: int = 8):
443
  super().__init__()
 
448
 
449
 
450
  class ThresholdALU(nn.Module):
451
+ """Complete trainable interface + frozen circuits."""
 
 
 
452
 
453
  def __init__(self, device: str = 'cuda'):
454
  super().__init__()
 
465
 
466
  def forward(self, a_bits_in: torch.Tensor, b_bits_in: torch.Tensor,
467
  op_onehot: torch.Tensor) -> torch.Tensor:
468
+ """Forward pass through trainable interface + frozen circuits."""
 
 
 
 
 
 
 
 
 
 
469
  x = torch.cat([a_bits_in, b_bits_in, op_onehot], dim=-1)
470
 
471
  a_bits, b_bits = self.encoder(x)
 
487
 
488
 
489
  class DirectCircuitModel(nn.Module):
490
+ """Minimal model that directly uses circuits without learned encoding."""
 
 
 
491
 
492
  def __init__(self, device: str = 'cuda'):
493
  super().__init__()
 
500
 
501
 
502
  class HiddenStateExtractor(nn.Module):
503
+ """Extracts operands and operation from LLM hidden states."""
 
 
 
504
 
505
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256):
506
  super().__init__()
 
524
  )
525
 
526
  def forward(self, hidden_states: torch.Tensor):
527
+ """Extract the operand bits and the operation from the hidden states."""
 
 
 
 
 
 
 
 
528
  a_logits = self.a_extractor(hidden_states)
529
  b_logits = self.b_extractor(hidden_states)
530
  op_logits = self.op_router(hidden_states)
 
542
 
543
 
544
  class AttentionPooling(nn.Module):
545
+ """Learnable attention pooling over sequence positions."""
 
 
 
546
 
547
  def __init__(self, hidden_dim: int = 960, num_heads: int = 4):
548
  super().__init__()
 
557
  self.cls_token = nn.Parameter(torch.randn(1, 1, hidden_dim) * 0.02)
558
 
559
  def forward(self, embeddings: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
560
+ """Pool the embeddings over the positions the mask attends."""
 
 
 
 
 
 
 
561
  batch_size, seq_len, hidden_dim = embeddings.shape
562
 
563
  cls_expanded = self.cls_token.expand(batch_size, -1, -1)
 
592
 
593
 
594
  class MultiHeadBitExtractor(nn.Module):
595
+ """8 separate extractors for 8 bits - each bit gets its own specialized network."""
 
 
 
596
 
597
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 128):
598
  super().__init__()
 
607
  ])
608
 
609
  def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
610
+ """Predict the eight bits of each operand from the hidden states."""
 
 
 
 
 
 
611
  hidden_states = torch.nan_to_num(hidden_states, nan=0.0)
612
 
613
  bit_logits = [extractor(hidden_states) for extractor in self.bit_extractors]
 
622
 
623
 
624
  class Extractor(nn.Module):
625
+ """Extracts operands and operation from LLM hidden states."""
 
 
 
626
 
627
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
628
  super().__init__()
 
639
  )
640
 
641
  def forward(self, embeddings: torch.Tensor, mask: torch.Tensor):
642
+ """Extract the operands and the operation from the pooled sequence."""
 
 
 
 
 
 
 
 
 
643
  pooled = self.attention_pool(embeddings, mask)
644
 
645
  a_bits, _ = self.a_extractor(pooled)
 
650
 
651
 
652
  class PositionExtractor(nn.Module):
653
+ """Position-specific extraction with dynamic operator detection."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
 
655
  DIGIT_TOKENS = set(range(32, 42))
656
  OPERATOR_TOKENS = {
 
691
  )
692
 
693
  def _find_operator_position(self, token_ids: torch.Tensor) -> tuple[int, int]:
694
+ """Find operator token position and its operation index."""
 
 
 
 
 
 
 
 
695
  for pos, tid in enumerate(token_ids.tolist()):
696
  if tid in self.OPERATOR_TOKENS:
697
  return pos, self.OPERATOR_TOKENS[tid]
698
  return -1, -1
699
 
700
  def _extract_digit_features(self, hidden: torch.Tensor, start: int, end: int) -> torch.Tensor:
701
+ """Extract and pad digit hidden states to fixed size."""
 
 
 
 
 
 
 
 
 
 
 
702
  n_digits = end - start
703
  features = torch.zeros(self.MAX_DIGITS * self.hidden_dim, device=hidden.device)
704
 
 
710
  return features
711
 
712
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
713
+ """Extract the operands at the positions around the detected operator token."""
 
 
 
 
 
 
 
 
 
 
714
  if token_ids is None:
715
  raise ValueError("PositionExtractor requires token_ids for operator detection")
716
 
 
771
 
772
 
773
  class PositionalDigitExtractor(nn.Module):
774
+ """Position-aware digit extraction: classifies each digit position independently."""
 
 
 
 
 
 
 
 
 
 
 
 
775
 
776
  DIGIT_TOKENS = set(range(32, 42))
777
  OPERATOR_TOKENS = {
 
849
  return torch.stack(bits)
850
 
851
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor):
852
+ """Classify each digit position of both operands."""
 
 
 
 
 
 
 
 
 
 
 
 
 
853
  batch_size = hidden.shape[0]
854
  device = hidden.device
855
 
 
903
 
904
 
905
  class DigitExtractor(nn.Module):
906
+ """Digit-level extraction: predicts digits (0-9) then converts to bits."""
 
 
 
907
 
908
  def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
909
  super().__init__()
 
929
  )
930
 
931
  def digits_to_bits(self, digit_logits: torch.Tensor) -> torch.Tensor:
932
+ """Convert the digit logits of three digits, [batch, 30], to 8-bit operands."""
 
 
 
 
933
  batch_size = digit_logits.shape[0]
934
 
935
  logits = digit_logits.view(batch_size, 3, 10)
 
953
  return torch.stack(bits, dim=-1)
954
 
955
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor):
956
+ """Return the operand bits, the operation logits and the digit logits."""
 
 
 
957
  pooled = self.attention_pool(hidden, mask)
958
 
959
  a_digit_logits = self.a_digit_pred(pooled)
 
967
 
968
 
969
  class HybridExtractor(nn.Module):
970
+ """Hybrid extractor that handles both digit tokens and word numbers."""
 
 
 
 
 
 
 
 
971
 
972
  DIGIT_TOKENS = set(range(32, 42))
973
  SYMBOL_OP_TOKENS = {
 
1033
  return -1
1034
 
1035
  def _extract_from_digits(self, token_ids: torch.Tensor) -> tuple:
1036
+ """Extract values directly from digit tokens (hardcoded lookup)."""
 
 
 
 
1037
  tokens = token_ids.tolist()
1038
 
1039
  op_pos = -1
 
1082
  return bits
1083
 
1084
  def _digits_to_value_and_bits(self, digit_logits: torch.Tensor, device) -> tuple:
1085
+ """Convert the 30 logits of three digits to a value and its 8 bits."""
 
 
 
 
1086
  logits = digit_logits.view(3, 10)
1087
  probs = torch.softmax(logits, dim=-1)
1088
 
 
1105
  return hundreds * 100 + tens * 10 + ones, torch.stack(bits)
1106
 
1107
  def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
1108
+ """Extract the operands from digit tokens by lookup, or from the hidden states."""
 
 
 
 
 
 
 
 
 
1109
  batch_size = hidden.shape[0]
1110
  device = hidden.device
1111
 
 
1215
 
1216
 
1217
  class ArithmeticModel(nn.Module):
1218
+ """LLM + extractor + frozen threshold circuits."""
 
 
 
1219
 
1220
  def __init__(self, device: str = 'cuda', unfreeze_layers: int = 0,
1221
  extract_layer: int = -1, position_extract: bool = False,
 
1310
  print(f" Total trainable: {total_trainable:,}", flush=True)
1311
 
1312
  def get_hidden_states(self, texts: list[str]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
1313
+ """Get hidden states from specified layer."""
 
 
 
 
 
 
 
1314
  inputs = self.tokenizer(
1315
  texts,
1316
  return_tensors='pt',
 
1332
  return hidden, mask, token_ids
1333
 
1334
  def forward(self, texts: list[str]):
1335
+ """Run text through the LLM, the extractor and the circuits to the result."""
 
 
 
 
 
 
 
 
1336
  hidden, mask, token_ids = self.get_hidden_states(texts)
1337
 
1338
  if self.hybrid or self.positional_digit or self.position_extract:
 
1385
  return params
1386
 
1387
 
1388
+ DEVICE = 'cuda'
1389
+
1390
+ ONES = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
1391
+ 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
1392
+ 'seventeen', 'eighteen', 'nineteen']
1393
+
1394
+ TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
1395
+
1396
+ def int_to_words(n: int) -> str:
1397
+ """Convert integer 0-255 to English words."""
1398
+ if n < 0 or n > 255:
1399
+ return str(n)
1400
+ if n < 20:
1401
+ return ONES[n]
1402
+ if n < 100:
1403
+ if n % 10 == 0:
1404
+ return TENS[n // 10]
1405
+ return f"{TENS[n // 10]} {ONES[n % 10]}"
1406
+ if n % 100 == 0:
1407
+ return f"{ONES[n // 100]} hundred"
1408
+ if n % 100 < 20:
1409
+ return f"{ONES[n // 100]} hundred {ONES[n % 100]}"
1410
+ if n % 10 == 0:
1411
+ return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]}"
1412
+ return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]} {ONES[n % 10]}"
1413
+
1414
+
1415
+ def train_int_to_bits(val: int, device: str = 'cuda') -> torch.Tensor:
1416
+ bits = torch.zeros(8, device=device)
1417
+ for i in range(8):
1418
+ bits[7-i] = (val >> i) & 1
1419
+ return bits
1420
+
1421
+
1422
+ def train_bits_to_int(bits: torch.Tensor) -> int:
1423
+ val = 0
1424
+ for i in range(8):
1425
+ if bits[i].item() > 0.5:
1426
+ val += 1 << (7-i)
1427
+ return val
1428
+
1429
+
1430
+ NL_TEMPLATES = {
1431
+ 'add': [
1432
+ "What is {a} plus {b}?",
1433
+ "Calculate {a} + {b}",
1434
+ "Add {a} and {b}",
1435
+ "What's the sum of {a} and {b}?",
1436
+ "If I have {a} and get {b} more, how many total?",
1437
+ "{a} + {b} = ?",
1438
+ "Compute {a} plus {b}",
1439
+ ],
1440
+ 'sub': [
1441
+ "What is {a} minus {b}?",
1442
+ "Calculate {a} - {b}",
1443
+ "Subtract {b} from {a}",
1444
+ "What's {a} take away {b}?",
1445
+ "If I have {a} and lose {b}, how many left?",
1446
+ "{a} - {b} = ?",
1447
+ "Compute {a} minus {b}",
1448
+ ],
1449
+ 'mul': [
1450
+ "What is {a} times {b}?",
1451
+ "Calculate {a} * {b}",
1452
+ "Multiply {a} by {b}",
1453
+ "What's {a} multiplied by {b}?",
1454
+ "{a} * {b} = ?",
1455
+ "Compute {a} times {b}",
1456
+ "What is the product of {a} and {b}?",
1457
+ ],
1458
+ 'gt': [
1459
+ "Is {a} greater than {b}?",
1460
+ "Is {a} > {b}?",
1461
+ "Check if {a} is larger than {b}",
1462
+ "Compare: is {a} more than {b}?",
1463
+ "{a} > {b}?",
1464
+ ],
1465
+ 'lt': [
1466
+ "Is {a} less than {b}?",
1467
+ "Is {a} < {b}?",
1468
+ "Check if {a} is smaller than {b}",
1469
+ "Compare: is {a} fewer than {b}?",
1470
+ "{a} < {b}?",
1471
+ ],
1472
+ 'eq': [
1473
+ "Is {a} equal to {b}?",
1474
+ "Is {a} == {b}?",
1475
+ "Does {a} equal {b}?",
1476
+ "Check if {a} equals {b}",
1477
+ "Are {a} and {b} the same?",
1478
+ ],
1479
+ }
1480
+
1481
+
1482
+ def generate_problem(max_val: int = 255):
1483
+ """Generate a random arithmetic problem for LLM training."""
1484
+ a = random.randint(0, max_val)
1485
+ b = random.randint(0, max_val)
1486
+ op = random.choice(OPERATIONS)
1487
+
1488
+ fmt = random.choice(['digits', 'words', 'nl_digits', 'nl_words'])
1489
+
1490
+ if fmt == 'digits':
1491
+ sym = OP_SYMBOLS[op]
1492
+ text = f"{a} {sym} {b}"
1493
+ elif fmt == 'words':
1494
+ a_word = int_to_words(a)
1495
+ b_word = int_to_words(b)
1496
+ op_word = {'add': 'plus', 'sub': 'minus', 'mul': 'times',
1497
+ 'gt': 'greater than', 'lt': 'less than', 'eq': 'equals'}[op]
1498
+ text = f"{a_word} {op_word} {b_word}"
1499
+ elif fmt == 'nl_digits':
1500
+ template = random.choice(NL_TEMPLATES[op])
1501
+ text = template.format(a=a, b=b)
1502
+ else:
1503
+ template = random.choice(NL_TEMPLATES[op])
1504
+ text = template.format(a=int_to_words(a), b=int_to_words(b))
1505
+
1506
+ if op == 'add':
1507
+ result = (a + b) & 0xFF
1508
+ elif op == 'sub':
1509
+ result = (a - b) & 0xFF
1510
+ elif op == 'mul':
1511
+ result = (a * b) & 0xFF
1512
+ elif op == 'gt':
1513
+ result = 1 if a > b else 0
1514
+ elif op == 'lt':
1515
+ result = 1 if a < b else 0
1516
+ elif op == 'eq':
1517
+ result = 1 if a == b else 0
1518
+
1519
+ return text, a, b, op, result
1520
+
1521
+
1522
+ def get_curriculum_max(epoch: int, total_epochs: int) -> int:
1523
+ """Curriculum learning: start with small numbers, gradually increase."""
1524
+ progress = epoch / total_epochs
1525
+ if progress < 0.2:
1526
+ return 9
1527
+ elif progress < 0.5:
1528
+ return 99
1529
+ else:
1530
+ return 255
1531
+
1532
+
1533
+ def train_router(epochs: int = 100, batch_size: int = 256, lr: float = 1e-2, device: str = 'cuda'):
1534
+ """Train only the router with ground truth bits."""
1535
+ print("=" * 70)
1536
+ print(" ROUTER-ONLY TRAINING (Ground Truth Bits)")
1537
+ print("=" * 70)
1538
+
1539
+ circuits = FrozenThresholdCircuits(device=device)
1540
+ router = OpRouter(input_dim=16 + 6, hidden_dim=64, n_ops=6).to(device)
1541
+
1542
+ print(f"\nRouter parameters: {sum(p.numel() for p in router.parameters()):,}")
1543
+
1544
+ def model_fn(a_bits, b_bits, op_onehot):
1545
+ x = torch.cat([a_bits, b_bits, op_onehot], dim=-1)
1546
+ op_weights = router(x)
1547
+ return circuits(a_bits, b_bits, op_weights)
1548
+
1549
+ initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
1550
+ print(f"Initial fitness: {initial_fitness:.4f}")
1551
+
1552
+ optimizer = optim.AdamW(router.parameters(), lr=lr)
1553
+ scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
1554
+
1555
+ print("\nTraining...")
1556
+ print("-" * 70)
1557
+
1558
+ best_fitness = initial_fitness
1559
+ start_time = time.perf_counter()
1560
+
1561
+ for epoch in range(epochs):
1562
+ router.train()
1563
+ epoch_loss = 0.0
1564
+
1565
+ for _ in range(100):
1566
+ batch = generate_batch(batch_size, device)
1567
+
1568
+ optimizer.zero_grad()
1569
+
1570
+ x = torch.cat([batch['a_bits'], batch['b_bits'], batch['op_onehot']], dim=-1)
1571
+ op_weights = router(x)
1572
+ pred_bits = circuits(batch['a_bits'], batch['b_bits'], op_weights)
1573
+
1574
+ loss = compute_loss(pred_bits, batch['expected_bits'])
1575
+ loss.backward()
1576
+ optimizer.step()
1577
+
1578
+ epoch_loss += loss.item()
1579
+
1580
+ scheduler.step()
1581
+
1582
+ if (epoch + 1) % 10 == 0 or epoch == 0:
1583
+ router.eval()
1584
+ fitness, details = compute_fitness(model_fn, n_samples=2000, device=device, return_details=True)
1585
+ elapsed = time.perf_counter() - start_time
1586
+
1587
+ if fitness > best_fitness:
1588
+ best_fitness = fitness
1589
+ marker = " *"
1590
+ else:
1591
+ marker = ""
1592
+
1593
+ print(f"Epoch {epoch+1:3d} | Loss: {epoch_loss/100:.4f} | "
1594
+ f"Fitness: {fitness:.4f}{marker} | Time: {elapsed:.1f}s")
1595
+
1596
+ if fitness >= 0.9999:
1597
+ print("\n TARGET: 100% FITNESS ACHIEVED")
1598
+ break
1599
+
1600
+ print("\n" + "=" * 70)
1601
+ print(" RESULTS")
1602
+ print("=" * 70)
1603
+
1604
+ router.eval()
1605
+ final_fitness, details = compute_fitness(model_fn, n_samples=5000, device=device, return_details=True)
1606
+
1607
+ print(f"\nFinal fitness: {final_fitness:.4f}")
1608
+ print(f"\nPer-operation:")
1609
+ for op in OPERATIONS:
1610
+ acc = details['by_op'][op]['accuracy']
1611
+ print(f" {op}: {acc:.4f}")
1612
+
1613
+ print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
1614
+
1615
+ if final_fitness >= 0.99:
1616
+ print("\nCONCLUSION: Router successfully learned operation dispatch.")
1617
+ print(" With correct bit encoding, 100% is achievable.")
1618
+
1619
+ save_path = os.path.join(TRAINED, 'router.pt')
1620
+ torch.save({
1621
+ 'router_state_dict': router.state_dict(),
1622
+ 'final_fitness': final_fitness,
1623
+ 'params': sum(p.numel() for p in router.parameters()),
1624
+ }, save_path)
1625
+ print(f"\nSaved trained router to: {save_path}")
1626
+
1627
+ return router, final_fitness
1628
+
1629
+
1630
+ def get_gpu_memory():
1631
+ """Get GPU memory usage in MB."""
1632
+ if torch.cuda.is_available():
1633
+ return torch.cuda.memory_allocated() / 1024 / 1024, torch.cuda.max_memory_allocated() / 1024 / 1024
1634
+ return 0, 0
1635
+
1636
+
1637
+ def train_interface(epochs: int = 200, batch_size: int = 512, lr: float = 1e-3,
1638
+ eval_interval: int = 10, device: str = 'cuda'):
1639
+ """Train BitEncoder + OpRouter with ground truth bits."""
1640
+ print("=" * 70)
1641
+ print(" INTERFACE TRAINING (Encoder + Router)")
1642
+ print("=" * 70)
1643
+ print(f" Started at: {time.strftime('%H:%M:%S')}")
1644
+
1645
+ print("\n[1/4] Verifying frozen circuits...")
1646
+ print(" Creating DirectCircuitModel...", end=" ", flush=True)
1647
+ direct_model = DirectCircuitModel(device=device)
1648
+ mem, max_mem = get_gpu_memory()
1649
+ print(f"done. VRAM: {mem:.0f}MB")
1650
+
1651
+ def direct_fn(a, b, op):
1652
+ return direct_model(a, b, op)
1653
+
1654
+ print(" Running fitness check (1000 samples)...", end=" ", flush=True)
1655
+ circuit_fitness = compute_fitness(direct_fn, n_samples=1000, device=device)
1656
+ print(f"done. Fitness: {circuit_fitness:.4f}")
1657
+ if circuit_fitness < 0.999:
1658
+ print(" ERROR: Circuits not achieving 100%. Aborting.")
1659
+ return None, 0.0
1660
+ print(" STATUS: PASS")
1661
+
1662
+ print("\n[2/4] Initializing model...")
1663
+ print(" Creating ThresholdALU...", end=" ", flush=True)
1664
+ model = ThresholdALU(device=device)
1665
+ mem, max_mem = get_gpu_memory()
1666
+ print(f"done. VRAM: {mem:.0f}MB")
1667
+
1668
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
1669
+ print(f" Trainable parameters: {trainable_params:,}")
1670
+
1671
+ def model_fn(a, b, op):
1672
+ return model(a, b, op)
1673
+
1674
+ print(" Running initial fitness check...", end=" ", flush=True)
1675
+ initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
1676
+ print(f"done. Fitness: {initial_fitness:.4f}")
1677
+
1678
+ print("\n[3/4] Setting up training...")
1679
+ print(" Creating optimizer...", end=" ", flush=True)
1680
+ optimizer = optim.AdamW(model.parameters(), lr=lr)
1681
+ print("done.")
1682
+ print(" Creating scheduler...", end=" ", flush=True)
1683
+ scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
1684
+ print("done.")
1685
+
1686
+ print(f" Config: lr={lr}, batch_size={batch_size}, epochs={epochs}")
1687
+
1688
+ print("\n[4/4] Training...")
1689
+ print(" Generating first batch to warm up...", end=" ", flush=True)
1690
+ warmup_batch = generate_batch(batch_size, device)
1691
+ mem, max_mem = get_gpu_memory()
1692
+ print(f"done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
1693
+
1694
+ print("-" * 70)
1695
+
1696
+ best_fitness = initial_fitness
1697
+ start_time = time.perf_counter()
1698
+ n_batches = 100
1699
+
1700
+ for epoch in range(epochs):
1701
+ model.train()
1702
+ epoch_loss = 0.0
1703
+ epoch_start = time.perf_counter()
1704
+
1705
+ for batch_idx in range(n_batches):
1706
+ batch = generate_batch(batch_size, device)
1707
+
1708
+ optimizer.zero_grad()
1709
+
1710
+ pred_bits = model(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
1711
+
1712
+ loss = compute_loss(pred_bits, batch['expected_bits'])
1713
+
1714
+ loss.backward()
1715
+ optimizer.step()
1716
+
1717
+ epoch_loss += loss.item()
1718
+
1719
+ if batch_idx == 0 and epoch == 0:
1720
+ mem, max_mem = get_gpu_memory()
1721
+ print(f" First forward/backward done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
1722
+
1723
+ if (batch_idx + 1) % 25 == 0:
1724
+ avg_so_far = epoch_loss / (batch_idx + 1)
1725
+ print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
1726
+
1727
+ scheduler.step()
1728
+
1729
+ avg_loss = epoch_loss / n_batches
1730
+ epoch_time = time.perf_counter() - epoch_start
1731
+
1732
+ if (epoch + 1) % 5 == 0 or epoch == 0: # Eval every 5 epochs
1733
+ model.eval()
1734
+ fitness, details = compute_fitness(
1735
+ model_fn, n_samples=2000, device=device, return_details=True
1736
+ )
1737
+
1738
+ elapsed = time.perf_counter() - start_time
1739
+
1740
+ if fitness > best_fitness:
1741
+ best_fitness = fitness
1742
+ marker = " *"
1743
+ else:
1744
+ marker = ""
1745
+
1746
+ mem, _ = get_gpu_memory()
1747
+ print(f"Epoch {epoch+1:4d} | Loss: {avg_loss:.4f} | "
1748
+ f"Fitness: {fitness:.4f}{marker} | "
1749
+ f"LR: {scheduler.get_last_lr()[0]:.2e} | "
1750
+ f"VRAM: {mem:.0f}MB | "
1751
+ f"Time: {elapsed:.1f}s ({epoch_time:.1f}s/epoch)")
1752
+
1753
+ if fitness >= 0.9999:
1754
+ print("\n" + "=" * 70)
1755
+ print(" TARGET ACHIEVED: 100% FITNESS")
1756
+ print("=" * 70)
1757
+ break
1758
+
1759
+ print("\n" + "=" * 70)
1760
+ print(" TRAINING COMPLETE")
1761
+ print("=" * 70)
1762
+
1763
+ model.eval()
1764
+ final_fitness, details = compute_fitness(
1765
+ model_fn, n_samples=5000, device=device, return_details=True
1766
+ )
1767
+
1768
+ print(f"\nFinal fitness: {final_fitness:.4f}")
1769
+ print(f"Best fitness: {best_fitness:.4f}")
1770
+ print(f"\nPer-operation breakdown:")
1771
+ for op in OPERATIONS:
1772
+ acc = details['by_op'][op]['accuracy']
1773
+ print(f" {op:6}: {acc:.4f}")
1774
+
1775
+ print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
1776
+
1777
+ save_path = os.path.join(TRAINED, 'interface.pt')
1778
+ torch.save({
1779
+ 'encoder_state_dict': model.encoder.state_dict(),
1780
+ 'router_state_dict': model.router.state_dict(),
1781
+ 'final_fitness': final_fitness,
1782
+ 'best_fitness': best_fitness,
1783
+ }, save_path)
1784
+ print(f"\nSaved trained model to: {save_path}")
1785
+
1786
+ return model, final_fitness
1787
+
1788
+
1789
+ def compute_llm_loss(pred_bits, a_bits, b_bits, op_logits,
1790
+ target_result, target_a, target_b, target_op_idx,
1791
+ bit_weight: float = 2.0):
1792
+ """Multi-component loss for LLM training; bit_weight scales the operand-bit losses."""
1793
+ result_loss = nn.functional.binary_cross_entropy_with_logits(
1794
+ pred_bits, target_result
1795
+ )
1796
+
1797
+ a_bits_safe = torch.clamp(a_bits, 0.0, 1.0)
1798
+ b_bits_safe = torch.clamp(b_bits, 0.0, 1.0)
1799
+ a_bits_safe = torch.nan_to_num(a_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
1800
+ b_bits_safe = torch.nan_to_num(b_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
1801
+
1802
+ a_loss = nn.functional.binary_cross_entropy(
1803
+ torch.clamp(a_bits_safe, 1e-6, 1-1e-6), target_a
1804
+ )
1805
+ b_loss = nn.functional.binary_cross_entropy(
1806
+ torch.clamp(b_bits_safe, 1e-6, 1-1e-6), target_b
1807
+ )
1808
+
1809
+ op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
1810
+
1811
+ total = result_loss + bit_weight * a_loss + bit_weight * b_loss + op_loss
1812
+ total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
1813
+
1814
+ return total, {
1815
+ 'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
1816
+ 'a': a_loss.item() if not torch.isnan(a_loss) else 10.0,
1817
+ 'b': b_loss.item() if not torch.isnan(b_loss) else 10.0,
1818
+ 'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
1819
+ }
1820
+
1821
+
1822
+ def value_to_digits(value: int) -> list:
1823
+ """Convert integer value to list of digits (hundreds, tens, ones)."""
1824
+ digits = []
1825
+ for place in [100, 10, 1]:
1826
+ digit = (value // place) % 10
1827
+ digits.append(digit)
1828
+ return digits
1829
+
1830
+
1831
+ def compute_positional_digit_loss(pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
1832
+ target_result, target_op_idx, target_a_values, target_b_values,
1833
+ device, digit_weight: float = 5.0):
1834
+ """Loss for positional digit extraction with DIRECT digit supervision."""
1835
+ result_loss = nn.functional.binary_cross_entropy_with_logits(
1836
+ pred_bits, target_result
1837
+ )
1838
+
1839
+ op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
1840
+
1841
+ a_digit_loss = torch.tensor(0.0, device=device)
1842
+ b_digit_loss = torch.tensor(0.0, device=device)
1843
+ n_a_digits = 0
1844
+ n_b_digits = 0
1845
+
1846
+ for i, (a_logits_list, b_logits_list) in enumerate(zip(a_digit_logits_list, b_digit_logits_list)):
1847
+ target_a = target_a_values[i].item()
1848
+ target_b = target_b_values[i].item()
1849
+
1850
+ a_digits = value_to_digits(int(target_a))
1851
+ b_digits = value_to_digits(int(target_b))
1852
+
1853
+ n_a = len(a_logits_list)
1854
+ n_b = len(b_logits_list)
1855
+
1856
+ if n_a > 0:
1857
+ target_a_digits = a_digits[-n_a:]
1858
+ for j, logits in enumerate(a_logits_list):
1859
+ target_digit = torch.tensor([target_a_digits[j]], device=device, dtype=torch.long)
1860
+ a_digit_loss = a_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
1861
+ n_a_digits += 1
1862
+
1863
+ if n_b > 0:
1864
+ target_b_digits = b_digits[-n_b:]
1865
+ for j, logits in enumerate(b_logits_list):
1866
+ target_digit = torch.tensor([target_b_digits[j]], device=device, dtype=torch.long)
1867
+ b_digit_loss = b_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
1868
+ n_b_digits += 1
1869
+
1870
+ if n_a_digits > 0:
1871
+ a_digit_loss = a_digit_loss / n_a_digits
1872
+ if n_b_digits > 0:
1873
+ b_digit_loss = b_digit_loss / n_b_digits
1874
+
1875
+ total = result_loss + digit_weight * a_digit_loss + digit_weight * b_digit_loss + op_loss
1876
+ total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
1877
+
1878
+ return total, {
1879
+ 'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
1880
+ 'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
1881
+ 'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
1882
+ 'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
1883
+ }
1884
+
1885
+
1886
+ def compute_hybrid_loss(pred_bits, op_logits, used_lookup,
1887
+ a_digit_logits, b_digit_logits,
1888
+ target_result, target_a_values, target_b_values, target_op_idx,
1889
+ device, digit_weight: float = 2.0):
1890
+ """Loss for hybrid extraction with digit-level prediction."""
1891
+ result_loss = nn.functional.binary_cross_entropy_with_logits(
1892
+ pred_bits, target_result
1893
+ )
1894
+
1895
+ op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
1896
+
1897
+ word_mask = ~used_lookup
1898
+ n_words = word_mask.sum().item()
1899
+
1900
+ if n_words > 0 and a_digit_logits is not None and b_digit_logits is not None:
1901
+ target_a_word = target_a_values[word_mask].long()
1902
+ target_b_word = target_b_values[word_mask].long()
1903
+
1904
+ a_hundreds = target_a_word // 100
1905
+ a_tens = (target_a_word % 100) // 10
1906
+ a_ones = target_a_word % 10
1907
+
1908
+ b_hundreds = target_b_word // 100
1909
+ b_tens = (target_b_word % 100) // 10
1910
+ b_ones = target_b_word % 10
1911
+
1912
+ a_logits = a_digit_logits.view(-1, 3, 10)
1913
+ b_logits = b_digit_logits.view(-1, 3, 10)
1914
+
1915
+ a_digit_loss = (
1916
+ nn.functional.cross_entropy(a_logits[:, 0], a_hundreds) +
1917
+ nn.functional.cross_entropy(a_logits[:, 1], a_tens) +
1918
+ nn.functional.cross_entropy(a_logits[:, 2], a_ones)
1919
+ ) / 3.0
1920
+
1921
+ b_digit_loss = (
1922
+ nn.functional.cross_entropy(b_logits[:, 0], b_hundreds) +
1923
+ nn.functional.cross_entropy(b_logits[:, 1], b_tens) +
1924
+ nn.functional.cross_entropy(b_logits[:, 2], b_ones)
1925
+ ) / 3.0
1926
+ else:
1927
+ a_digit_loss = torch.tensor(0.0, device=device)
1928
+ b_digit_loss = torch.tensor(0.0, device=device)
1929
+
1930
+ total = result_loss + op_loss + digit_weight * (a_digit_loss + b_digit_loss)
1931
+ total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
1932
+
1933
+ return total, {
1934
+ 'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
1935
+ 'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
1936
+ 'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
1937
+ 'op': op_loss.item() if not torch.isnan(op_loss) else 10.0,
1938
+ 'n_words': n_words,
1939
+ 'n_lookup': used_lookup.sum().item()
1940
+ }
1941
+
1942
+
1943
+ def evaluate_llm(model, n_samples: int = 500):
1944
+ """Evaluate LLM model on random problems (mixed digit/word format)."""
1945
+ model.extractor.eval()
1946
+ correct = 0
1947
+ op_correct = 0
1948
+
1949
+ for _ in range(n_samples):
1950
+ text, a, b, op, expected = generate_problem()
1951
+
1952
+ with torch.no_grad():
1953
+ outputs = model([text])
1954
+ result_bits = outputs[0]
1955
+ op_logits = outputs[3]
1956
+
1957
+ pred_result = train_bits_to_int(result_bits[0])
1958
+ pred_op = OPERATIONS[op_logits[0].argmax().item()]
1959
+
1960
+ if pred_result == expected:
1961
+ correct += 1
1962
+ if pred_op == op:
1963
+ op_correct += 1
1964
+
1965
+ model.extractor.train()
1966
+ return correct / n_samples, op_correct / n_samples
1967
+
1968
+
1969
+ def train_llm(epochs: int = 100, batch_size: int = 256, lr: float = 3e-4,
1970
+ unfreeze_layers: int = 0, extract_layer: int = -1,
1971
+ position_extract: bool = False, digit_pred: bool = False,
1972
+ positional_digit: bool = False, device: str = 'cuda'):
1973
+ """Train extractor with LLM hidden states."""
1974
+ hybrid = not (positional_digit or position_extract or digit_pred)
1975
+
1976
+ print("=" * 70)
1977
+ print(" LLM TRAINING")
1978
+ if unfreeze_layers > 0:
1979
+ print(f" {unfreeze_layers} transformer layers unfrozen")
1980
+ else:
1981
+ print(" LLM frozen")
1982
+ if extract_layer != -1:
1983
+ print(f" Extracting from layer {extract_layer}")
1984
+ if hybrid:
1985
+ print(" HYBRID extraction (digit lookup + word learning)")
1986
+ elif positional_digit:
1987
+ print(" POSITIONAL DIGIT extraction (legacy, 100% on digits only)")
1988
+ elif position_extract:
1989
+ print(" Position-specific extraction (legacy)")
1990
+ elif digit_pred:
1991
+ print(" Digit-level prediction (legacy)")
1992
+ print("=" * 70)
1993
+
1994
+ print("\nInitializing model...")
1995
+ model = ArithmeticModel(
1996
+ device=device,
1997
+ unfreeze_layers=unfreeze_layers,
1998
+ extract_layer=extract_layer,
1999
+ position_extract=position_extract,
2000
+ digit_pred=digit_pred,
2001
+ positional_digit=positional_digit,
2002
+ hybrid=hybrid
2003
+ )
2004
+
2005
+ optimizer = optim.AdamW(model.trainable_parameters(), lr=lr)
2006
+ scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
2007
+
2008
+ print(f"\nTraining config:")
2009
+ print(f" Epochs: {epochs}")
2010
+ print(f" Batch size: {batch_size}")
2011
+ print(f" Learning rate: {lr}")
2012
+ print(f" Unfreeze layers: {unfreeze_layers}")
2013
+ print(f" Samples/epoch: {batch_size * 20}")
2014
+
2015
+ print(f"\nInitial evaluation (200 samples)...")
2016
+ acc, op_acc = evaluate_llm(model, 200)
2017
+ print(f" Accuracy: {acc:.4f}, Op accuracy: {op_acc:.4f}")
2018
+
2019
+ print(f"\nStarting training...")
2020
+ print("-" * 70)
2021
+
2022
+ best_acc = acc
2023
+ start_time = time.perf_counter()
2024
+
2025
+ for epoch in range(epochs):
2026
+ model.extractor.train()
2027
+ if unfreeze_layers > 0:
2028
+ model.llm.train()
2029
+
2030
+ max_val = get_curriculum_max(epoch, epochs)
2031
+
2032
+ epoch_loss = 0
2033
+ if hybrid:
2034
+ epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0, 'n_words': 0, 'n_lookup': 0}
2035
+ elif positional_digit:
2036
+ epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0}
2037
+ else:
2038
+ epoch_losses = {'result': 0, 'a': 0, 'b': 0, 'op': 0}
2039
+ n_batches = 20
2040
+ epoch_start = time.perf_counter()
2041
+
2042
+ for batch_idx in range(n_batches):
2043
+ batch_texts = []
2044
+ batch_a = []
2045
+ batch_b = []
2046
+ batch_op = []
2047
+ batch_result = []
2048
+ batch_a_values = []
2049
+ batch_b_values = []
2050
+
2051
+ for _ in range(batch_size):
2052
+ text, a, b, op, result = generate_problem(max_val)
2053
+ batch_texts.append(text)
2054
+ batch_a.append(train_int_to_bits(a, device))
2055
+ batch_b.append(train_int_to_bits(b, device))
2056
+ batch_op.append(OPERATIONS.index(op))
2057
+ batch_result.append(train_int_to_bits(result, device))
2058
+ batch_a_values.append(a)
2059
+ batch_b_values.append(b)
2060
+
2061
+ target_a = torch.stack(batch_a)
2062
+ target_b = torch.stack(batch_b)
2063
+ target_op = torch.tensor(batch_op, device=device)
2064
+ target_result = torch.stack(batch_result)
2065
+ target_a_values = torch.tensor(batch_a_values, device=device, dtype=torch.float32)
2066
+ target_b_values = torch.tensor(batch_b_values, device=device, dtype=torch.float32)
2067
+
2068
+ optimizer.zero_grad()
2069
+
2070
+ outputs = model(batch_texts)
2071
+ pred_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
2072
+
2073
+ if hybrid:
2074
+ a_values, b_values, used_lookup = outputs[4], outputs[5], outputs[6]
2075
+ a_digit_logits, b_digit_logits = outputs[7], outputs[8]
2076
+ loss, losses = compute_hybrid_loss(
2077
+ pred_bits, op_logits, used_lookup,
2078
+ a_digit_logits, b_digit_logits,
2079
+ target_result, target_a_values, target_b_values, target_op, device
2080
+ )
2081
+ elif positional_digit:
2082
+ a_digit_logits_list = outputs[7]
2083
+ b_digit_logits_list = outputs[8]
2084
+ loss, losses = compute_positional_digit_loss(
2085
+ pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
2086
+ target_result, target_op, target_a_values, target_b_values, device
2087
+ )
2088
+ else:
2089
+ loss, losses = compute_llm_loss(
2090
+ pred_bits, a_bits, b_bits, op_logits,
2091
+ target_result, target_a, target_b, target_op
2092
+ )
2093
+
2094
+ loss.backward()
2095
+ torch.nn.utils.clip_grad_norm_(model.trainable_parameters(), 1.0)
2096
+ optimizer.step()
2097
+
2098
+ epoch_loss += loss.item()
2099
+ for k in epoch_losses:
2100
+ epoch_losses[k] += losses[k]
2101
+
2102
+ if (batch_idx + 1) % 5 == 0:
2103
+ avg_so_far = epoch_loss / (batch_idx + 1)
2104
+ print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
2105
+
2106
+ epoch_time = time.perf_counter() - epoch_start
2107
+ scheduler.step()
2108
+
2109
+ avg_loss = epoch_loss / n_batches
2110
+ for k in epoch_losses:
2111
+ epoch_losses[k] /= n_batches
2112
+
2113
+ acc, op_acc = evaluate_llm(model, 300)
2114
+ elapsed = time.perf_counter() - start_time
2115
+
2116
+ marker = " *" if acc > best_acc else ""
2117
+ if acc > best_acc:
2118
+ best_acc = acc
2119
+
2120
+ mem, _ = get_gpu_memory()
2121
+ print(f"Epoch {epoch+1:3d} | Loss: {avg_loss:.4f} | "
2122
+ f"Acc: {acc:.4f}{marker} | OpAcc: {op_acc:.4f} | "
2123
+ f"Range: 0-{max_val} | VRAM: {mem:.0f}MB | Time: {elapsed:.0f}s")
2124
+ if hybrid:
2125
+ print(f" Losses - result:{epoch_losses['result']:.4f} "
2126
+ f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
2127
+ f"op:{epoch_losses['op']:.4f} | words:{epoch_losses['n_words']:.0f} lookup:{epoch_losses['n_lookup']:.0f}")
2128
+ elif positional_digit:
2129
+ print(f" Losses - result:{epoch_losses['result']:.4f} "
2130
+ f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
2131
+ f"op:{epoch_losses['op']:.4f}")
2132
+ else:
2133
+ print(f" Losses - result:{epoch_losses['result']:.4f} "
2134
+ f"a:{epoch_losses['a']:.4f} b:{epoch_losses['b']:.4f} "
2135
+ f"op:{epoch_losses['op']:.4f}")
2136
+
2137
+ if acc >= 0.99:
2138
+ print("\n" + "=" * 70)
2139
+ print(" TARGET ACHIEVED: 99%+ ACCURACY")
2140
+ print("=" * 70)
2141
+ break
2142
+
2143
+ print("\n" + "=" * 70)
2144
+ print(" FINAL EVALUATION")
2145
+ print("=" * 70)
2146
+
2147
+ acc, op_acc = evaluate_llm(model, 1000)
2148
+ print(f"Final accuracy: {acc:.4f}")
2149
+ print(f"Final op accuracy: {op_acc:.4f}")
2150
+ print(f"Best accuracy: {best_acc:.4f}")
2151
+
2152
+ print("\nSample predictions:")
2153
+ for _ in range(10):
2154
+ text, a, b, op, expected = generate_problem()
2155
+ with torch.no_grad():
2156
+ outputs = model([text])
2157
+ result_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
2158
+ pred = train_bits_to_int(result_bits[0])
2159
+ pred_a = train_bits_to_int(a_bits[0])
2160
+ pred_b = train_bits_to_int(b_bits[0])
2161
+ pred_op = OPERATIONS[op_logits[0].argmax().item()]
2162
+
2163
+ status = "OK" if pred == expected else "WRONG"
2164
+ print(f" '{text}' = {expected} | pred={pred} (a={pred_a}, b={pred_b}, op={pred_op}) [{status}]")
2165
+
2166
+ save_path = os.path.join(TRAINED, 'llm.pt')
2167
+ save_dict = {
2168
+ 'extractor_state_dict': model.extractor.state_dict(),
2169
+ 'final_accuracy': acc,
2170
+ 'best_accuracy': best_acc,
2171
+ 'unfreeze_layers': unfreeze_layers,
2172
+ }
2173
+ if unfreeze_layers > 0:
2174
+ save_dict['llm_state_dict'] = {
2175
+ k: v for k, v in model.llm.state_dict().items()
2176
+ if any(f'layers.{i}.' in k for i in range(len(model.llm.model.layers) - unfreeze_layers, len(model.llm.model.layers)))
2177
+ }
2178
+ torch.save(save_dict, save_path)
2179
+ print(f"\nSaved to: {save_path}")
2180
+
2181
+ return model, acc
2182
+
2183
+
2184
+ def main_train():
2185
+ parser = argparse.ArgumentParser(
2186
+ description='Unified training for threshold circuit LLM integration',
2187
+ formatter_class=argparse.RawDescriptionHelpFormatter,
2188
+ epilog="""
2189
+ Modes:
2190
+ router - Train only OpRouter with ground truth bits (sanity check)
2191
+ interface - Train BitEncoder + OpRouter with ground truth bits (sanity check)
2192
+ llm - Train extractor with LLM hidden states (the real training)
2193
+
2194
+ LLM options:
2195
+ --unfreeze_layers N Fine-tune top N transformer layers
2196
+ --extract_layer N Extract from layer N (-1 = last)
2197
+ --position_extract Use position-specific extraction
2198
+ --digit_pred Predict digits instead of bits
2199
+
2200
+ Baked-in: curriculum learning (0-9 -> 0-99 -> 0-255), 2x loss weight for a/b
2201
+
2202
+ Examples:
2203
+ python train.py --mode llm --epochs 100
2204
+ python train.py --mode llm --position_extract
2205
+ python train.py --mode llm --digit_pred --extract_layer 12
2206
+ python train.py --mode llm --unfreeze_layers 4 --batch_size 4096
2207
+ """
2208
+ )
2209
+ parser.add_argument('--mode', type=str, required=True,
2210
+ choices=['router', 'interface', 'llm'],
2211
+ help='Training mode')
2212
+ parser.add_argument('--epochs', type=int, default=100, help='Number of epochs')
2213
+ parser.add_argument('--batch_size', type=int, default=512, help='Batch size (default: 512)')
2214
+ parser.add_argument('--lr', type=float, default=None,
2215
+ help='Learning rate (default: mode-specific)')
2216
+ parser.add_argument('--unfreeze_layers', type=int, default=0,
2217
+ help='Unfreeze top N transformer layers (default 0 = frozen)')
2218
+ parser.add_argument('--extract_layer', type=int, default=0,
2219
+ help='Which layer to extract from (default: 0 = embeddings, best for digits)')
2220
+ parser.add_argument('--position_extract', action='store_true',
2221
+ help='Use position-specific extraction (legacy)')
2222
+ parser.add_argument('--digit_pred', action='store_true',
2223
+ help='Predict digits instead of bits (legacy)')
2224
+ parser.add_argument('--positional_digit', action='store_true', default=False,
2225
+ help='Use positional digit extraction (legacy, 100%% on digits only)')
2226
+ parser.add_argument('--device', type=str, default='cuda', help='Device')
2227
+ args = parser.parse_args()
2228
+
2229
+ torch.manual_seed(42)
2230
+ random.seed(42)
2231
+
2232
+ if args.mode == 'router':
2233
+ lr = args.lr if args.lr is not None else 1e-2
2234
+ train_router(epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device)
2235
+
2236
+ elif args.mode == 'interface':
2237
+ lr = args.lr if args.lr is not None else 1e-3
2238
+ model, fitness = train_interface(
2239
+ epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device
2240
+ )
2241
+
2242
+ print("\n" + "=" * 70)
2243
+ print(" EXPERIMENT SUMMARY")
2244
+ print("=" * 70)
2245
+ print(f"\n Control (Vanilla SmolLM2-360M): 11.90%")
2246
+ print(f" Experimental (Trained Interface): {100*fitness:.2f}%")
2247
+ if fitness > 0:
2248
+ print(f"\n Improvement: {100*(fitness - 0.119)/0.119:.1f}%")
2249
+
2250
+ if fitness >= 0.99:
2251
+ print("\n CONCLUSION: Frozen threshold circuits + trained interface")
2252
+ print(" achieves near-perfect arithmetic accuracy.")
2253
+ print(" Core thesis VALIDATED.")
2254
+ else:
2255
+ print(f"\n CONCLUSION: Further training or architecture changes needed.")
2256
+ print(f" Current gap: {100*(1.0 - fitness):.2f}%")
2257
+
2258
+ elif args.mode == 'llm':
2259
+ lr = args.lr if args.lr is not None else 3e-4
2260
+ train_llm(
2261
+ epochs=args.epochs,
2262
+ batch_size=args.batch_size,
2263
+ lr=lr,
2264
+ unfreeze_layers=args.unfreeze_layers,
2265
+ extract_layer=args.extract_layer,
2266
+ position_extract=args.position_extract,
2267
+ digit_pred=args.digit_pred,
2268
+ positional_digit=args.positional_digit,
2269
+ device=args.device
2270
+ )
2271
+
2272
+ DEVICE = "cuda"
2273
+
2274
+ MODEL_ID = "HuggingFaceTB/SmolLM2-360M-Instruct"
2275
+
2276
+ SYSTEM_PROMPT = """You are a calculator. Output only the numeric answer. No words, no explanation, just digits. Examples:
2277
+ User: 5 + 3
2278
+ Assistant: 8
2279
+ User: 12 * 7
2280
+ Assistant: 84
2281
+ User: 100 > 50
2282
+ Assistant: 1
2283
+ User: 25 < 10
2284
+ Assistant: 0"""
2285
+
2286
+
2287
+ def load_model():
2288
+ print(f"Loading {MODEL_ID}...")
2289
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
2290
+ tokenizer.padding_side = "left"
2291
+ if tokenizer.pad_token is None:
2292
+ tokenizer.pad_token = tokenizer.eos_token
2293
+ model = AutoModelForCausalLM.from_pretrained(
2294
+ MODEL_ID,
2295
+ torch_dtype=torch.float16,
2296
+ device_map=DEVICE
2297
+ )
2298
+ model.eval()
2299
+ print(f" Loaded. Parameters: {sum(p.numel() for p in model.parameters()):,}")
2300
+ return model, tokenizer
2301
+
2302
+
2303
+ def format_prompt(tokenizer, op_str):
2304
+ messages = [
2305
+ {"role": "system", "content": SYSTEM_PROMPT},
2306
+ {"role": "user", "content": op_str}
2307
+ ]
2308
+ return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
2309
+
2310
+
2311
+ def baseline_generate(model, tokenizer, prompts, max_new_tokens=16):
2312
+ inputs = tokenizer(prompts, return_tensors="pt", padding=True).to(DEVICE)
2313
+ with torch.no_grad():
2314
+ outputs = model.generate(
2315
+ **inputs,
2316
+ max_new_tokens=max_new_tokens,
2317
+ do_sample=False,
2318
+ pad_token_id=tokenizer.eos_token_id
2319
+ )
2320
+ responses = []
2321
+ for i, output in enumerate(outputs):
2322
+ response = tokenizer.decode(output[inputs.input_ids.shape[1]:], skip_special_tokens=True)
2323
+ responses.append(response.strip())
2324
+ return responses
2325
+
2326
+
2327
+ def extract_answer(text):
2328
+ """Generous extraction - find any number in output"""
2329
+ text = text.strip().lower()
2330
+ if not text:
2331
+ return None
2332
+
2333
+ # Handle Yes/No for comparisons
2334
+ if text in ['yes', 'true', '1']:
2335
+ return 1
2336
+ if text in ['no', 'false', '0']:
2337
+ return 0
2338
+ if text.startswith('yes'):
2339
+ return 1
2340
+ if text.startswith('no'):
2341
+ return 0
2342
+
2343
+ # Find all numbers, take the LAST one (most likely the answer)
2344
+ numbers = re.findall(r'-?\d+', text)
2345
+ if numbers:
2346
+ return int(numbers[-1])
2347
+ return None
2348
+
2349
+
2350
+ def baseline_ground_truth(a, b, op):
2351
+ """Compute expected result (8-bit where applicable)"""
2352
+ if op == 'add':
2353
+ return (a + b) & 0xFF
2354
+ elif op == 'sub':
2355
+ return (a - b) & 0xFF
2356
+ elif op == 'mul':
2357
+ return (a * b) & 0xFF
2358
+ elif op == 'div':
2359
+ return a // b if b != 0 else 0
2360
+ elif op == 'and':
2361
+ return a & b
2362
+ elif op == 'or':
2363
+ return a | b
2364
+ elif op == 'xor':
2365
+ return a ^ b
2366
+ elif op == 'gt':
2367
+ return 1 if a > b else 0
2368
+ elif op == 'lt':
2369
+ return 1 if a < b else 0
2370
+ elif op == 'eq':
2371
+ return 1 if a == b else 0
2372
+ elif op == 'ge':
2373
+ return 1 if a >= b else 0
2374
+ elif op == 'le':
2375
+ return 1 if a <= b else 0
2376
+ else:
2377
+ raise ValueError(f"Unknown op: {op}")
2378
+
2379
+
2380
+ def op_to_str(a, b, op):
2381
+ """Convert operation to natural string"""
2382
+ symbols = {
2383
+ 'add': '+', 'sub': '-', 'mul': '*', 'div': '/',
2384
+ 'and': '&', 'or': '|', 'xor': '^',
2385
+ 'gt': '>', 'lt': '<', 'eq': '==', 'ge': '>=', 'le': '<='
2386
+ }
2387
+ return f"{a} {symbols[op]} {b}"
2388
+
2389
+
2390
+ def evaluate(model, tokenizer, n_samples=1000, batch_size=32, ops=None):
2391
+ if ops is None:
2392
+ ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
2393
+
2394
+ results = {op: {'correct': 0, 'total': 0} for op in ops}
2395
+ all_correct = 0
2396
+ all_total = 0
2397
+
2398
+ samples = []
2399
+ for _ in range(n_samples):
2400
+ a = random.randint(0, 255)
2401
+ b = random.randint(0, 255)
2402
+ if 'div' in ops and random.random() < 0.1:
2403
+ op = 'div'
2404
+ b = random.randint(1, 255) # avoid div by zero
2405
+ else:
2406
+ op = random.choice([o for o in ops if o != 'div'])
2407
+ samples.append((a, b, op))
2408
+
2409
+ print(f"\nEvaluating {n_samples} samples (batch_size={batch_size})...")
2410
+
2411
+ for batch_start in range(0, n_samples, batch_size):
2412
+ batch = samples[batch_start:batch_start + batch_size]
2413
+ prompts = [format_prompt(tokenizer, op_to_str(a, b, op)) for a, b, op in batch]
2414
+ responses = baseline_generate(model, tokenizer, prompts)
2415
+
2416
+ for (a, b, op), response in zip(batch, responses):
2417
+ expected = baseline_ground_truth(a, b, op)
2418
+ extracted = extract_answer(response)
2419
+
2420
+ correct = (extracted == expected)
2421
+ results[op]['total'] += 1
2422
+ all_total += 1
2423
+ if correct:
2424
+ results[op]['correct'] += 1
2425
+ all_correct += 1
2426
+
2427
+ if (batch_start + batch_size) % 200 == 0 or batch_start + batch_size >= n_samples:
2428
+ pct = 100 * all_correct / all_total
2429
+ print(f" Progress: {min(batch_start + batch_size, n_samples)}/{n_samples} | Accuracy: {pct:.2f}%")
2430
+
2431
+ return results, all_correct, all_total
2432
+
2433
+
2434
+ def main_baseline():
2435
+ random.seed(42)
2436
+ torch.manual_seed(42)
2437
+
2438
+ model, tokenizer = load_model()
2439
+
2440
+ # Quick sanity check
2441
+ print("\nSanity check (5 examples):")
2442
+ test_cases = [
2443
+ ("5 + 3", 8),
2444
+ ("100 - 37", 63),
2445
+ ("12 * 11", 132),
2446
+ ("50 > 30", 1),
2447
+ ("25 < 10", 0),
2448
+ ]
2449
+ prompts = [format_prompt(tokenizer, q) for q, _ in test_cases]
2450
+ responses = baseline_generate(model, tokenizer, prompts)
2451
+ for (q, expected), response in zip(test_cases, responses):
2452
+ extracted = extract_answer(response)
2453
+ status = "OK" if extracted == expected else "FAIL"
2454
+ print(f" {q} = {expected} | Model: '{response}' -> {extracted} [{status}]")
2455
+
2456
+ # Full evaluation
2457
+ print("\n" + "=" * 60)
2458
+ print(" BASELINE EVALUATION")
2459
+ print("=" * 60)
2460
+
2461
+ ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
2462
+ results, correct, total = evaluate(model, tokenizer, n_samples=2000, batch_size=64, ops=ops)
2463
+
2464
+ print("\n" + "=" * 60)
2465
+ print(" RESULTS BY OPERATION")
2466
+ print("=" * 60)
2467
+ for op in ops:
2468
+ r = results[op]
2469
+ pct = 100 * r['correct'] / r['total'] if r['total'] > 0 else 0
2470
+ print(f" {op:6}: {r['correct']:4}/{r['total']:4} ({pct:6.2f}%)")
2471
+
2472
+ print("\n" + "=" * 60)
2473
+ print(" OVERALL")
2474
+ print("=" * 60)
2475
+ fitness = correct / total
2476
+ print(f" Correct: {correct}/{total}")
2477
+ print(f" Fitness: {fitness:.4f} ({100*fitness:.2f}%)")
2478
+ print("=" * 60)
2479
+
2480
+ return fitness
2481
+
2482
+
2483
+ def cmd_circuits():
2484
+ print("Testing frozen circuits...")
2485
+
2486
+ circuits = FrozenThresholdCircuits(device='cuda')
2487
+ print(f"Loaded {len(circuits.weights)} tensors")
2488
+
2489
+ a = torch.tensor([[0, 0, 0, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
2490
+ b = torch.tensor([[0, 0, 0, 0, 0, 0, 1, 1]], device='cuda', dtype=torch.float32)
2491
+
2492
+ result, carry = circuits.add_8bit(a, b)
2493
+ val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
2494
+ print(f"5 + 3 = {val} (expected 8)")
2495
+
2496
+ a = torch.tensor([[0, 1, 1, 0, 0, 1, 0, 0]], device='cuda', dtype=torch.float32)
2497
+ b = torch.tensor([[0, 0, 1, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
2498
+ result, _ = circuits.sub_8bit(a, b)
2499
+ val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
2500
+ print(f"100 - 37 = {val} (expected 63)")
2501
+
2502
+ a = torch.tensor([[0, 0, 0, 0, 1, 1, 0, 0]], device='cuda', dtype=torch.float32)
2503
+ b = torch.tensor([[0, 0, 0, 0, 1, 0, 1, 1]], device='cuda', dtype=torch.float32)
2504
+ result = circuits.mul_8bit(a, b)
2505
+ val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
2506
+ print(f"12 * 11 = {val} (expected 132)")
2507
+
2508
+ a = torch.tensor([[0, 0, 1, 1, 0, 0, 1, 0]], device='cuda', dtype=torch.float32)
2509
+ b = torch.tensor([[0, 0, 0, 1, 1, 1, 1, 0]], device='cuda', dtype=torch.float32)
2510
+ gt = circuits.compare_gt(a, b)
2511
+ lt = circuits.compare_lt(a, b)
2512
+ eq = circuits.compare_eq(a, b)
2513
+ print(f"50 > 30: {int(gt[0].item())} (expected 1)")
2514
+ print(f"50 < 30: {int(lt[0].item())} (expected 0)")
2515
+ print(f"50 == 30: {int(eq[0].item())} (expected 0)")
2516
+
2517
+ print("\nTesting batched forward...")
2518
+ batch_a = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
2519
+ batch_b = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
2520
+ op = torch.zeros(16, 6, device='cuda')
2521
+ op[:, 0] = 1.0
2522
+
2523
+ result = circuits(batch_a, batch_b, op)
2524
+ print(f"Batch output shape: {result.shape}")
2525
+ print("Done.")
2526
+
2527
+
2528
+ def cmd_fitness():
2529
+ print("Testing fitness module...")
2530
+
2531
+ batch = generate_batch(8, 'cpu')
2532
+ print(f"\nSample batch:")
2533
+ for i in range(4):
2534
+ a, b = batch['a'][i].item(), batch['b'][i].item()
2535
+ op = idx_to_op(batch['op'][i].item())
2536
+ expected = batch['expected'][i].item()
2537
+ print(f" {a} {op} {b} = {expected}")
2538
+
2539
+ def random_model(a_bits, b_bits, op_onehot):
2540
+ return torch.rand(a_bits.shape[0], 8, device=a_bits.device)
2541
+
2542
+ fitness = compute_fitness(random_model, n_samples=1000, batch_size=100, device='cpu')
2543
+ print(f"\nRandom model fitness: {fitness:.4f} (expected ~0.004 for 8-bit)")
2544
+
2545
+ def perfect_model(a_bits, b_bits, op_onehot):
2546
+ batch_size = a_bits.shape[0]
2547
+ results = torch.zeros(batch_size, 8, device=a_bits.device)
2548
+ for i in range(batch_size):
2549
+ a = sum(int(a_bits[i, j].item()) << (7-j) for j in range(8))
2550
+ b = sum(int(b_bits[i, j].item()) << (7-j) for j in range(8))
2551
+ op_idx = op_onehot[i].argmax().item()
2552
+ result = ground_truth(a, b, idx_to_op(op_idx))
2553
+ for j in range(8):
2554
+ results[i, 7-j] = (result >> j) & 1
2555
+ return results
2556
+
2557
+ fitness = compute_fitness(perfect_model, n_samples=1000, batch_size=100, device='cpu')
2558
+ print(f"Perfect model fitness: {fitness:.4f} (expected 1.0)")
2559
+
2560
+
2561
+ def cmd_model():
2562
  import sys
2563
  sys.path.insert(0, '.')
 
2564
 
2565
  print("Testing model components...")
2566
 
 
2611
  print(f" Total trainable: {total:,}")
2612
 
2613
  print("\nDone.")
2614
+
2615
+
2616
+ COMMANDS = {'circuits': cmd_circuits, 'fitness': cmd_fitness, 'model': cmd_model, 'train': main_train, 'baseline': main_baseline}
2617
+
2618
+
2619
+ if __name__ == "__main__":
2620
+ if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
2621
+ sys.exit('commands: ' + ', '.join(COMMANDS))
2622
+ cmd = sys.argv.pop(1)
2623
+ sys.argv[0] += ' ' + cmd
2624
+ sys.exit(COMMANDS[cmd]())
llm_integration/smollm2/analyze_smollm2.py DELETED
@@ -1,232 +0,0 @@
1
- """
2
- SmolLM2-360M-Instruct Architecture Analysis
3
- For 8bit-threshold-computer LLM Integration Project
4
- """
5
-
6
- import torch
7
- from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
8
- from collections import defaultdict
9
- import json
10
-
11
- def analyze_smollm2():
12
- model_name = "HuggingFaceTB/SmolLM2-360M-Instruct"
13
-
14
- print("=" * 80)
15
- print("SmolLM2-360M-Instruct Architecture Analysis")
16
- print("=" * 80)
17
-
18
- # Load config first
19
- print("\n[1] Loading model configuration...")
20
- config = AutoConfig.from_pretrained(model_name)
21
- print(f"Config loaded: {type(config).__name__}")
22
-
23
- # Load tokenizer
24
- print("\n[2] Loading tokenizer...")
25
- tokenizer = AutoTokenizer.from_pretrained(model_name)
26
- print(f"Tokenizer loaded: {type(tokenizer).__name__}")
27
-
28
- # Load model with hidden states output
29
- print("\n[3] Loading model with output_hidden_states=True...")
30
- model = AutoModelForCausalLM.from_pretrained(
31
- model_name,
32
- output_hidden_states=True,
33
- torch_dtype=torch.float32
34
- )
35
- model.eval()
36
- print(f"Model loaded: {type(model).__name__}")
37
-
38
- # ========================================================================
39
- # ARCHITECTURE CENSUS
40
- # ========================================================================
41
- print("\n" + "=" * 80)
42
- print("ARCHITECTURE CENSUS")
43
- print("=" * 80)
44
-
45
- print("\n--- Model Configuration ---")
46
- config_dict = config.to_dict()
47
- for key, value in sorted(config_dict.items()):
48
- print(f" {key}: {value}")
49
-
50
- print("\n--- Key Architecture Parameters ---")
51
- print(f" Model type: {config.model_type}")
52
- print(f" Vocabulary size: {config.vocab_size}")
53
- print(f" Hidden size: {config.hidden_size}")
54
- print(f" Intermediate size: {config.intermediate_size}")
55
- print(f" Number of hidden layers: {config.num_hidden_layers}")
56
- print(f" Number of attention heads: {config.num_attention_heads}")
57
- print(f" Number of KV heads: {getattr(config, 'num_key_value_heads', config.num_attention_heads)}")
58
- print(f" Head dimension: {config.hidden_size // config.num_attention_heads}")
59
- print(f" Max position embeddings: {config.max_position_embeddings}")
60
- print(f" RMS norm epsilon: {getattr(config, 'rms_norm_eps', 'N/A')}")
61
- print(f" Rope theta: {getattr(config, 'rope_theta', 'N/A')}")
62
- print(f" Tie word embeddings: {getattr(config, 'tie_word_embeddings', 'N/A')}")
63
-
64
- # ========================================================================
65
- # WEIGHT INVENTORY
66
- # ========================================================================
67
- print("\n" + "=" * 80)
68
- print("WEIGHT INVENTORY")
69
- print("=" * 80)
70
-
71
- total_params = 0
72
- param_groups = defaultdict(list)
73
-
74
- for name, param in model.named_parameters():
75
- total_params += param.numel()
76
-
77
- # Group by component
78
- if "embed_tokens" in name:
79
- group = "Embedding"
80
- elif "lm_head" in name:
81
- group = "LM Head"
82
- elif "norm" in name and "layers" not in name:
83
- group = "Final Norm"
84
- elif "layers" in name:
85
- layer_num = name.split(".")[2]
86
- if "self_attn" in name:
87
- group = f"Layer {layer_num} - Attention"
88
- elif "mlp" in name:
89
- group = f"Layer {layer_num} - MLP"
90
- elif "norm" in name:
91
- group = f"Layer {layer_num} - Norms"
92
- else:
93
- group = f"Layer {layer_num} - Other"
94
- else:
95
- group = "Other"
96
-
97
- param_groups[group].append({
98
- "name": name,
99
- "shape": tuple(param.shape),
100
- "numel": param.numel(),
101
- "dtype": str(param.dtype)
102
- })
103
-
104
- print(f"\n--- Total Parameters: {total_params:,} ---")
105
- print(f" ({total_params / 1e6:.2f}M parameters)")
106
-
107
- # Print by group
108
- for group_name in sorted(param_groups.keys()):
109
- params = param_groups[group_name]
110
- group_total = sum(p["numel"] for p in params)
111
- print(f"\n### {group_name} ({group_total:,} params, {group_total/total_params*100:.2f}%)")
112
- for p in params:
113
- print(f" {p['name']}")
114
- print(f" Shape: {p['shape']}, Elements: {p['numel']:,}, Dtype: {p['dtype']}")
115
-
116
- # ========================================================================
117
- # TOKENIZATION ANALYSIS
118
- # ========================================================================
119
- print("\n" + "=" * 80)
120
- print("TOKENIZATION ANALYSIS")
121
- print("=" * 80)
122
-
123
- test_input = "47 + 86"
124
- print(f"\n--- Test Input: '{test_input}' ---")
125
-
126
- tokens = tokenizer(test_input, return_tensors="pt")
127
- input_ids = tokens["input_ids"][0]
128
-
129
- print(f"\nInput IDs: {input_ids.tolist()}")
130
- print(f"Number of tokens: {len(input_ids)}")
131
-
132
- print("\nToken breakdown:")
133
- for i, token_id in enumerate(input_ids):
134
- token_str = tokenizer.decode([token_id])
135
- print(f" Position {i}: ID={token_id.item():5d}, Token='{token_str}'")
136
-
137
- # Additional tokenization tests
138
- print("\n--- Additional Tokenization Tests ---")
139
- test_cases = ["0", "1", "47", "86", "133", " + ", "="]
140
- for tc in test_cases:
141
- ids = tokenizer.encode(tc, add_special_tokens=False)
142
- decoded = [tokenizer.decode([i]) for i in ids]
143
- print(f" '{tc}' -> IDs: {ids}, Tokens: {decoded}")
144
-
145
- # ========================================================================
146
- # HIDDEN STATE ANALYSIS
147
- # ========================================================================
148
- print("\n" + "=" * 80)
149
- print("HIDDEN STATE ANALYSIS")
150
- print("=" * 80)
151
-
152
- print(f"\n--- Running inference on '{test_input}' ---")
153
-
154
- with torch.no_grad():
155
- outputs = model(**tokens)
156
-
157
- hidden_states = outputs.hidden_states
158
- print(f"\nNumber of hidden state outputs: {len(hidden_states)}")
159
- print("(This includes embedding output + each layer's output)")
160
-
161
- print("\nHidden state shapes at each layer:")
162
- for i, hs in enumerate(hidden_states):
163
- layer_name = "Embedding" if i == 0 else f"Layer {i-1}"
164
- print(f" {layer_name}: {tuple(hs.shape)}")
165
- if i == 0:
166
- print(f" (batch_size=1, seq_len={hs.shape[1]}, hidden_dim={hs.shape[2]})")
167
-
168
- # Analyze hidden state statistics at different layers
169
- print("\n--- Hidden State Statistics (per layer) ---")
170
- for i, hs in enumerate(hidden_states):
171
- layer_name = "Embedding" if i == 0 else f"Layer {i-1}"
172
- hs_flat = hs.view(-1)
173
- print(f" {layer_name}:")
174
- print(f" Mean: {hs_flat.mean().item():.6f}")
175
- print(f" Std: {hs_flat.std().item():.6f}")
176
- print(f" Min: {hs_flat.min().item():.6f}")
177
- print(f" Max: {hs_flat.max().item():.6f}")
178
-
179
- # ========================================================================
180
- # MODEL STRUCTURE DEEP DIVE
181
- # ========================================================================
182
- print("\n" + "=" * 80)
183
- print("MODEL STRUCTURE DEEP DIVE")
184
- print("=" * 80)
185
-
186
- print("\n--- Model Architecture String ---")
187
- print(model)
188
-
189
- # ========================================================================
190
- # SUMMARY DATA FOR REPORT
191
- # ========================================================================
192
- summary = {
193
- "model_name": model_name,
194
- "total_params": total_params,
195
- "config": {
196
- "vocab_size": config.vocab_size,
197
- "hidden_size": config.hidden_size,
198
- "intermediate_size": config.intermediate_size,
199
- "num_hidden_layers": config.num_hidden_layers,
200
- "num_attention_heads": config.num_attention_heads,
201
- "num_kv_heads": getattr(config, 'num_key_value_heads', config.num_attention_heads),
202
- "head_dim": config.hidden_size // config.num_attention_heads,
203
- "max_position_embeddings": config.max_position_embeddings,
204
- "rms_norm_eps": getattr(config, 'rms_norm_eps', None),
205
- "rope_theta": getattr(config, 'rope_theta', None),
206
- "tie_word_embeddings": getattr(config, 'tie_word_embeddings', None),
207
- },
208
- "tokenization": {
209
- "test_input": test_input,
210
- "token_ids": input_ids.tolist(),
211
- "num_tokens": len(input_ids),
212
- "tokens": [tokenizer.decode([tid]) for tid in input_ids]
213
- },
214
- "hidden_states": {
215
- "num_outputs": len(hidden_states),
216
- "shape": list(hidden_states[0].shape)
217
- },
218
- "param_groups": {k: {"count": len(v), "total": sum(p["numel"] for p in v)} for k, v in param_groups.items()}
219
- }
220
-
221
- # Save summary as JSON for report generation
222
- with open("D:/8bit-threshold-computer/llm_integration/smollm2_analysis.json", "w") as f:
223
- json.dump(summary, f, indent=2)
224
-
225
- print("\n" + "=" * 80)
226
- print("Analysis complete. Summary saved to smollm2_analysis.json")
227
- print("=" * 80)
228
-
229
- return summary, model, tokenizer, hidden_states, param_groups
230
-
231
- if __name__ == "__main__":
232
- summary, model, tokenizer, hidden_states, param_groups = analyze_smollm2()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
llm_integration/train.py DELETED
@@ -1,955 +0,0 @@
1
- """
2
- Unified training script for threshold circuit LLM integration.
3
-
4
- Modes:
5
- --mode router : Train only OpRouter with ground truth bits (sanity check)
6
- --mode interface : Train BitEncoder + OpRouter with ground truth bits (sanity check)
7
- --mode llm : Train extractor with LLM hidden states (the real training)
8
-
9
- LLM mode options:
10
- --unfreeze_layers N : Unfreeze top N transformer layers (default 0 = fully frozen)
11
-
12
- Hardware Profile (NVIDIA RTX 6000 Ada 48GB):
13
- VRAM Scaling (unfreeze_layers=4):
14
- batch_size | VRAM | %
15
- -----------+---------+------
16
- 512 | 5,784 | 11.8%
17
- 1,024 | 7,384 | 15.0%
18
- 4,096 | 16,534 | 33.6%
19
- 13,000 | 39,000 | 79.4% <-- recommended for 80% target
20
-
21
- Examples:
22
- python train.py --mode llm --epochs 100 --batch_size 256
23
- python train.py --mode llm --epochs 100 --batch_size 4096 --unfreeze_layers 4
24
- """
25
-
26
- import torch
27
- import torch.nn as nn
28
- import torch.optim as optim
29
- import time
30
- import argparse
31
- import random
32
-
33
- from model import (
34
- ThresholdALU, DirectCircuitModel, OpRouter,
35
- ArithmeticModel, OPERATIONS, OP_SYMBOLS
36
- )
37
- from circuits import FrozenThresholdCircuits
38
- from fitness import generate_batch, compute_fitness, compute_loss
39
-
40
- DEVICE = 'cuda'
41
-
42
- ONES = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
43
- 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
44
- 'seventeen', 'eighteen', 'nineteen']
45
- TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
46
-
47
- def int_to_words(n: int) -> str:
48
- """Convert integer 0-255 to English words."""
49
- if n < 0 or n > 255:
50
- return str(n)
51
- if n < 20:
52
- return ONES[n]
53
- if n < 100:
54
- if n % 10 == 0:
55
- return TENS[n // 10]
56
- return f"{TENS[n // 10]} {ONES[n % 10]}"
57
- if n % 100 == 0:
58
- return f"{ONES[n // 100]} hundred"
59
- if n % 100 < 20:
60
- return f"{ONES[n // 100]} hundred {ONES[n % 100]}"
61
- if n % 10 == 0:
62
- return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]}"
63
- return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]} {ONES[n % 10]}"
64
-
65
-
66
- def int_to_bits(val: int, device: str = 'cuda') -> torch.Tensor:
67
- bits = torch.zeros(8, device=device)
68
- for i in range(8):
69
- bits[7-i] = (val >> i) & 1
70
- return bits
71
-
72
-
73
- def bits_to_int(bits: torch.Tensor) -> int:
74
- val = 0
75
- for i in range(8):
76
- if bits[i].item() > 0.5:
77
- val += 1 << (7-i)
78
- return val
79
-
80
-
81
- NL_TEMPLATES = {
82
- 'add': [
83
- "What is {a} plus {b}?",
84
- "Calculate {a} + {b}",
85
- "Add {a} and {b}",
86
- "What's the sum of {a} and {b}?",
87
- "If I have {a} and get {b} more, how many total?",
88
- "{a} + {b} = ?",
89
- "Compute {a} plus {b}",
90
- ],
91
- 'sub': [
92
- "What is {a} minus {b}?",
93
- "Calculate {a} - {b}",
94
- "Subtract {b} from {a}",
95
- "What's {a} take away {b}?",
96
- "If I have {a} and lose {b}, how many left?",
97
- "{a} - {b} = ?",
98
- "Compute {a} minus {b}",
99
- ],
100
- 'mul': [
101
- "What is {a} times {b}?",
102
- "Calculate {a} * {b}",
103
- "Multiply {a} by {b}",
104
- "What's {a} multiplied by {b}?",
105
- "{a} * {b} = ?",
106
- "Compute {a} times {b}",
107
- "What is the product of {a} and {b}?",
108
- ],
109
- 'gt': [
110
- "Is {a} greater than {b}?",
111
- "Is {a} > {b}?",
112
- "Check if {a} is larger than {b}",
113
- "Compare: is {a} more than {b}?",
114
- "{a} > {b}?",
115
- ],
116
- 'lt': [
117
- "Is {a} less than {b}?",
118
- "Is {a} < {b}?",
119
- "Check if {a} is smaller than {b}",
120
- "Compare: is {a} fewer than {b}?",
121
- "{a} < {b}?",
122
- ],
123
- 'eq': [
124
- "Is {a} equal to {b}?",
125
- "Is {a} == {b}?",
126
- "Does {a} equal {b}?",
127
- "Check if {a} equals {b}",
128
- "Are {a} and {b} the same?",
129
- ],
130
- }
131
-
132
-
133
- def generate_problem(max_val: int = 255):
134
- """
135
- Generate a random arithmetic problem for LLM training.
136
- Randomly mixes digit and word formats.
137
- """
138
- a = random.randint(0, max_val)
139
- b = random.randint(0, max_val)
140
- op = random.choice(OPERATIONS)
141
-
142
- fmt = random.choice(['digits', 'words', 'nl_digits', 'nl_words'])
143
-
144
- if fmt == 'digits':
145
- sym = OP_SYMBOLS[op]
146
- text = f"{a} {sym} {b}"
147
- elif fmt == 'words':
148
- a_word = int_to_words(a)
149
- b_word = int_to_words(b)
150
- op_word = {'add': 'plus', 'sub': 'minus', 'mul': 'times',
151
- 'gt': 'greater than', 'lt': 'less than', 'eq': 'equals'}[op]
152
- text = f"{a_word} {op_word} {b_word}"
153
- elif fmt == 'nl_digits':
154
- template = random.choice(NL_TEMPLATES[op])
155
- text = template.format(a=a, b=b)
156
- else:
157
- template = random.choice(NL_TEMPLATES[op])
158
- text = template.format(a=int_to_words(a), b=int_to_words(b))
159
-
160
- if op == 'add':
161
- result = (a + b) & 0xFF
162
- elif op == 'sub':
163
- result = (a - b) & 0xFF
164
- elif op == 'mul':
165
- result = (a * b) & 0xFF
166
- elif op == 'gt':
167
- result = 1 if a > b else 0
168
- elif op == 'lt':
169
- result = 1 if a < b else 0
170
- elif op == 'eq':
171
- result = 1 if a == b else 0
172
-
173
- return text, a, b, op, result
174
-
175
-
176
- def get_curriculum_max(epoch: int, total_epochs: int) -> int:
177
- """
178
- Curriculum learning: start with small numbers, gradually increase.
179
- Epoch 0-20%: 0-9 (single digit)
180
- Epoch 20-50%: 0-99 (two digit)
181
- Epoch 50-100%: 0-255 (full range)
182
- """
183
- progress = epoch / total_epochs
184
- if progress < 0.2:
185
- return 9
186
- elif progress < 0.5:
187
- return 99
188
- else:
189
- return 255
190
-
191
-
192
- def train_router(epochs: int = 100, batch_size: int = 256, lr: float = 1e-2, device: str = 'cuda'):
193
- """Train only the router with ground truth bits."""
194
- print("=" * 70)
195
- print(" ROUTER-ONLY TRAINING (Ground Truth Bits)")
196
- print("=" * 70)
197
-
198
- circuits = FrozenThresholdCircuits(device=device)
199
- router = OpRouter(input_dim=16 + 6, hidden_dim=64, n_ops=6).to(device)
200
-
201
- print(f"\nRouter parameters: {sum(p.numel() for p in router.parameters()):,}")
202
-
203
- def model_fn(a_bits, b_bits, op_onehot):
204
- x = torch.cat([a_bits, b_bits, op_onehot], dim=-1)
205
- op_weights = router(x)
206
- return circuits(a_bits, b_bits, op_weights)
207
-
208
- initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
209
- print(f"Initial fitness: {initial_fitness:.4f}")
210
-
211
- optimizer = optim.AdamW(router.parameters(), lr=lr)
212
- scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
213
-
214
- print("\nTraining...")
215
- print("-" * 70)
216
-
217
- best_fitness = initial_fitness
218
- start_time = time.perf_counter()
219
-
220
- for epoch in range(epochs):
221
- router.train()
222
- epoch_loss = 0.0
223
-
224
- for _ in range(100):
225
- batch = generate_batch(batch_size, device)
226
-
227
- optimizer.zero_grad()
228
-
229
- x = torch.cat([batch['a_bits'], batch['b_bits'], batch['op_onehot']], dim=-1)
230
- op_weights = router(x)
231
- pred_bits = circuits(batch['a_bits'], batch['b_bits'], op_weights)
232
-
233
- loss = compute_loss(pred_bits, batch['expected_bits'])
234
- loss.backward()
235
- optimizer.step()
236
-
237
- epoch_loss += loss.item()
238
-
239
- scheduler.step()
240
-
241
- if (epoch + 1) % 10 == 0 or epoch == 0:
242
- router.eval()
243
- fitness, details = compute_fitness(model_fn, n_samples=2000, device=device, return_details=True)
244
- elapsed = time.perf_counter() - start_time
245
-
246
- if fitness > best_fitness:
247
- best_fitness = fitness
248
- marker = " *"
249
- else:
250
- marker = ""
251
-
252
- print(f"Epoch {epoch+1:3d} | Loss: {epoch_loss/100:.4f} | "
253
- f"Fitness: {fitness:.4f}{marker} | Time: {elapsed:.1f}s")
254
-
255
- if fitness >= 0.9999:
256
- print("\n TARGET: 100% FITNESS ACHIEVED")
257
- break
258
-
259
- print("\n" + "=" * 70)
260
- print(" RESULTS")
261
- print("=" * 70)
262
-
263
- router.eval()
264
- final_fitness, details = compute_fitness(model_fn, n_samples=5000, device=device, return_details=True)
265
-
266
- print(f"\nFinal fitness: {final_fitness:.4f}")
267
- print(f"\nPer-operation:")
268
- for op in OPERATIONS:
269
- acc = details['by_op'][op]['accuracy']
270
- print(f" {op}: {acc:.4f}")
271
-
272
- print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
273
-
274
- if final_fitness >= 0.99:
275
- print("\nCONCLUSION: Router successfully learned operation dispatch.")
276
- print(" With correct bit encoding, 100% is achievable.")
277
-
278
- save_path = "D:/8bit-threshold-computer/llm_integration/trained/router.pt"
279
- torch.save({
280
- 'router_state_dict': router.state_dict(),
281
- 'final_fitness': final_fitness,
282
- 'params': sum(p.numel() for p in router.parameters()),
283
- }, save_path)
284
- print(f"\nSaved trained router to: {save_path}")
285
-
286
- return router, final_fitness
287
-
288
-
289
- def get_gpu_memory():
290
- """Get GPU memory usage in MB."""
291
- if torch.cuda.is_available():
292
- return torch.cuda.memory_allocated() / 1024 / 1024, torch.cuda.max_memory_allocated() / 1024 / 1024
293
- return 0, 0
294
-
295
-
296
- def train_interface(epochs: int = 200, batch_size: int = 512, lr: float = 1e-3,
297
- eval_interval: int = 10, device: str = 'cuda'):
298
- """Train BitEncoder + OpRouter with ground truth bits."""
299
- print("=" * 70)
300
- print(" INTERFACE TRAINING (Encoder + Router)")
301
- print("=" * 70)
302
- print(f" Started at: {time.strftime('%H:%M:%S')}")
303
-
304
- print("\n[1/4] Verifying frozen circuits...")
305
- print(" Creating DirectCircuitModel...", end=" ", flush=True)
306
- direct_model = DirectCircuitModel(device=device)
307
- mem, max_mem = get_gpu_memory()
308
- print(f"done. VRAM: {mem:.0f}MB")
309
-
310
- def direct_fn(a, b, op):
311
- return direct_model(a, b, op)
312
-
313
- print(" Running fitness check (1000 samples)...", end=" ", flush=True)
314
- circuit_fitness = compute_fitness(direct_fn, n_samples=1000, device=device)
315
- print(f"done. Fitness: {circuit_fitness:.4f}")
316
- if circuit_fitness < 0.999:
317
- print(" ERROR: Circuits not achieving 100%. Aborting.")
318
- return None, 0.0
319
- print(" STATUS: PASS")
320
-
321
- print("\n[2/4] Initializing model...")
322
- print(" Creating ThresholdALU...", end=" ", flush=True)
323
- model = ThresholdALU(device=device)
324
- mem, max_mem = get_gpu_memory()
325
- print(f"done. VRAM: {mem:.0f}MB")
326
-
327
- trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
328
- print(f" Trainable parameters: {trainable_params:,}")
329
-
330
- def model_fn(a, b, op):
331
- return model(a, b, op)
332
-
333
- print(" Running initial fitness check...", end=" ", flush=True)
334
- initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
335
- print(f"done. Fitness: {initial_fitness:.4f}")
336
-
337
- print("\n[3/4] Setting up training...")
338
- print(" Creating optimizer...", end=" ", flush=True)
339
- optimizer = optim.AdamW(model.parameters(), lr=lr)
340
- print("done.")
341
- print(" Creating scheduler...", end=" ", flush=True)
342
- scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
343
- print("done.")
344
-
345
- print(f" Config: lr={lr}, batch_size={batch_size}, epochs={epochs}")
346
-
347
- print("\n[4/4] Training...")
348
- print(" Generating first batch to warm up...", end=" ", flush=True)
349
- warmup_batch = generate_batch(batch_size, device)
350
- mem, max_mem = get_gpu_memory()
351
- print(f"done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
352
-
353
- print("-" * 70)
354
-
355
- best_fitness = initial_fitness
356
- start_time = time.perf_counter()
357
- n_batches = 100
358
-
359
- for epoch in range(epochs):
360
- model.train()
361
- epoch_loss = 0.0
362
- epoch_start = time.perf_counter()
363
-
364
- for batch_idx in range(n_batches):
365
- batch = generate_batch(batch_size, device)
366
-
367
- optimizer.zero_grad()
368
-
369
- pred_bits = model(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
370
-
371
- loss = compute_loss(pred_bits, batch['expected_bits'])
372
-
373
- loss.backward()
374
- optimizer.step()
375
-
376
- epoch_loss += loss.item()
377
-
378
- if batch_idx == 0 and epoch == 0:
379
- mem, max_mem = get_gpu_memory()
380
- print(f" First forward/backward done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
381
-
382
- if (batch_idx + 1) % 25 == 0:
383
- avg_so_far = epoch_loss / (batch_idx + 1)
384
- print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
385
-
386
- scheduler.step()
387
-
388
- avg_loss = epoch_loss / n_batches
389
- epoch_time = time.perf_counter() - epoch_start
390
-
391
- if (epoch + 1) % 5 == 0 or epoch == 0: # Eval every 5 epochs
392
- model.eval()
393
- fitness, details = compute_fitness(
394
- model_fn, n_samples=2000, device=device, return_details=True
395
- )
396
-
397
- elapsed = time.perf_counter() - start_time
398
-
399
- if fitness > best_fitness:
400
- best_fitness = fitness
401
- marker = " *"
402
- else:
403
- marker = ""
404
-
405
- mem, _ = get_gpu_memory()
406
- print(f"Epoch {epoch+1:4d} | Loss: {avg_loss:.4f} | "
407
- f"Fitness: {fitness:.4f}{marker} | "
408
- f"LR: {scheduler.get_last_lr()[0]:.2e} | "
409
- f"VRAM: {mem:.0f}MB | "
410
- f"Time: {elapsed:.1f}s ({epoch_time:.1f}s/epoch)")
411
-
412
- if fitness >= 0.9999:
413
- print("\n" + "=" * 70)
414
- print(" TARGET ACHIEVED: 100% FITNESS")
415
- print("=" * 70)
416
- break
417
-
418
- print("\n" + "=" * 70)
419
- print(" TRAINING COMPLETE")
420
- print("=" * 70)
421
-
422
- model.eval()
423
- final_fitness, details = compute_fitness(
424
- model_fn, n_samples=5000, device=device, return_details=True
425
- )
426
-
427
- print(f"\nFinal fitness: {final_fitness:.4f}")
428
- print(f"Best fitness: {best_fitness:.4f}")
429
- print(f"\nPer-operation breakdown:")
430
- for op in OPERATIONS:
431
- acc = details['by_op'][op]['accuracy']
432
- print(f" {op:6}: {acc:.4f}")
433
-
434
- print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
435
-
436
- save_path = "D:/8bit-threshold-computer/llm_integration/trained/interface.pt"
437
- torch.save({
438
- 'encoder_state_dict': model.encoder.state_dict(),
439
- 'router_state_dict': model.router.state_dict(),
440
- 'final_fitness': final_fitness,
441
- 'best_fitness': best_fitness,
442
- }, save_path)
443
- print(f"\nSaved trained model to: {save_path}")
444
-
445
- return model, final_fitness
446
-
447
-
448
- def compute_llm_loss(pred_bits, a_bits, b_bits, op_logits,
449
- target_result, target_a, target_b, target_op_idx,
450
- bit_weight: float = 2.0):
451
- """
452
- Multi-component loss for LLM training.
453
- bit_weight: multiplier for a/b bit losses (default 2x since extraction is the bottleneck)
454
- """
455
- result_loss = nn.functional.binary_cross_entropy_with_logits(
456
- pred_bits, target_result
457
- )
458
-
459
- a_bits_safe = torch.clamp(a_bits, 0.0, 1.0)
460
- b_bits_safe = torch.clamp(b_bits, 0.0, 1.0)
461
- a_bits_safe = torch.nan_to_num(a_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
462
- b_bits_safe = torch.nan_to_num(b_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
463
-
464
- a_loss = nn.functional.binary_cross_entropy(
465
- torch.clamp(a_bits_safe, 1e-6, 1-1e-6), target_a
466
- )
467
- b_loss = nn.functional.binary_cross_entropy(
468
- torch.clamp(b_bits_safe, 1e-6, 1-1e-6), target_b
469
- )
470
-
471
- op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
472
-
473
- total = result_loss + bit_weight * a_loss + bit_weight * b_loss + op_loss
474
- total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
475
-
476
- return total, {
477
- 'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
478
- 'a': a_loss.item() if not torch.isnan(a_loss) else 10.0,
479
- 'b': b_loss.item() if not torch.isnan(b_loss) else 10.0,
480
- 'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
481
- }
482
-
483
-
484
- def value_to_digits(value: int) -> list:
485
- """Convert integer value to list of digits (hundreds, tens, ones)."""
486
- digits = []
487
- for place in [100, 10, 1]:
488
- digit = (value // place) % 10
489
- digits.append(digit)
490
- return digits
491
-
492
-
493
- def compute_positional_digit_loss(pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
494
- target_result, target_op_idx, target_a_values, target_b_values,
495
- device, digit_weight: float = 5.0):
496
- """
497
- Loss for positional digit extraction with DIRECT digit supervision.
498
-
499
- This provides strong gradients by directly supervising digit classification
500
- instead of going through the value -> bits conversion.
501
- """
502
- result_loss = nn.functional.binary_cross_entropy_with_logits(
503
- pred_bits, target_result
504
- )
505
-
506
- op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
507
-
508
- a_digit_loss = torch.tensor(0.0, device=device)
509
- b_digit_loss = torch.tensor(0.0, device=device)
510
- n_a_digits = 0
511
- n_b_digits = 0
512
-
513
- for i, (a_logits_list, b_logits_list) in enumerate(zip(a_digit_logits_list, b_digit_logits_list)):
514
- target_a = target_a_values[i].item()
515
- target_b = target_b_values[i].item()
516
-
517
- a_digits = value_to_digits(int(target_a))
518
- b_digits = value_to_digits(int(target_b))
519
-
520
- n_a = len(a_logits_list)
521
- n_b = len(b_logits_list)
522
-
523
- if n_a > 0:
524
- target_a_digits = a_digits[-n_a:]
525
- for j, logits in enumerate(a_logits_list):
526
- target_digit = torch.tensor([target_a_digits[j]], device=device, dtype=torch.long)
527
- a_digit_loss = a_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
528
- n_a_digits += 1
529
-
530
- if n_b > 0:
531
- target_b_digits = b_digits[-n_b:]
532
- for j, logits in enumerate(b_logits_list):
533
- target_digit = torch.tensor([target_b_digits[j]], device=device, dtype=torch.long)
534
- b_digit_loss = b_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
535
- n_b_digits += 1
536
-
537
- if n_a_digits > 0:
538
- a_digit_loss = a_digit_loss / n_a_digits
539
- if n_b_digits > 0:
540
- b_digit_loss = b_digit_loss / n_b_digits
541
-
542
- total = result_loss + digit_weight * a_digit_loss + digit_weight * b_digit_loss + op_loss
543
- total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
544
-
545
- return total, {
546
- 'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
547
- 'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
548
- 'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
549
- 'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
550
- }
551
-
552
-
553
- def compute_hybrid_loss(pred_bits, op_logits, used_lookup,
554
- a_digit_logits, b_digit_logits,
555
- target_result, target_a_values, target_b_values, target_op_idx,
556
- device, digit_weight: float = 2.0):
557
- """
558
- Loss for hybrid extraction with digit-level prediction.
559
-
560
- Uses cross-entropy on each digit (hundreds, tens, ones) for word samples.
561
- Samples using digit lookup are already 100% accurate - no loss computed.
562
- """
563
- result_loss = nn.functional.binary_cross_entropy_with_logits(
564
- pred_bits, target_result
565
- )
566
-
567
- op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
568
-
569
- word_mask = ~used_lookup
570
- n_words = word_mask.sum().item()
571
-
572
- if n_words > 0 and a_digit_logits is not None and b_digit_logits is not None:
573
- target_a_word = target_a_values[word_mask].long()
574
- target_b_word = target_b_values[word_mask].long()
575
-
576
- a_hundreds = target_a_word // 100
577
- a_tens = (target_a_word % 100) // 10
578
- a_ones = target_a_word % 10
579
-
580
- b_hundreds = target_b_word // 100
581
- b_tens = (target_b_word % 100) // 10
582
- b_ones = target_b_word % 10
583
-
584
- a_logits = a_digit_logits.view(-1, 3, 10)
585
- b_logits = b_digit_logits.view(-1, 3, 10)
586
-
587
- a_digit_loss = (
588
- nn.functional.cross_entropy(a_logits[:, 0], a_hundreds) +
589
- nn.functional.cross_entropy(a_logits[:, 1], a_tens) +
590
- nn.functional.cross_entropy(a_logits[:, 2], a_ones)
591
- ) / 3.0
592
-
593
- b_digit_loss = (
594
- nn.functional.cross_entropy(b_logits[:, 0], b_hundreds) +
595
- nn.functional.cross_entropy(b_logits[:, 1], b_tens) +
596
- nn.functional.cross_entropy(b_logits[:, 2], b_ones)
597
- ) / 3.0
598
- else:
599
- a_digit_loss = torch.tensor(0.0, device=device)
600
- b_digit_loss = torch.tensor(0.0, device=device)
601
-
602
- total = result_loss + op_loss + digit_weight * (a_digit_loss + b_digit_loss)
603
- total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
604
-
605
- return total, {
606
- 'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
607
- 'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
608
- 'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
609
- 'op': op_loss.item() if not torch.isnan(op_loss) else 10.0,
610
- 'n_words': n_words,
611
- 'n_lookup': used_lookup.sum().item()
612
- }
613
-
614
-
615
- def evaluate_llm(model, n_samples: int = 500):
616
- """Evaluate LLM model on random problems (mixed digit/word format)."""
617
- model.extractor.eval()
618
- correct = 0
619
- op_correct = 0
620
-
621
- for _ in range(n_samples):
622
- text, a, b, op, expected = generate_problem()
623
-
624
- with torch.no_grad():
625
- outputs = model([text])
626
- result_bits = outputs[0]
627
- op_logits = outputs[3]
628
-
629
- pred_result = bits_to_int(result_bits[0])
630
- pred_op = OPERATIONS[op_logits[0].argmax().item()]
631
-
632
- if pred_result == expected:
633
- correct += 1
634
- if pred_op == op:
635
- op_correct += 1
636
-
637
- model.extractor.train()
638
- return correct / n_samples, op_correct / n_samples
639
-
640
-
641
- def train_llm(epochs: int = 100, batch_size: int = 256, lr: float = 3e-4,
642
- unfreeze_layers: int = 0, extract_layer: int = -1,
643
- position_extract: bool = False, digit_pred: bool = False,
644
- positional_digit: bool = False, device: str = 'cuda'):
645
- """
646
- Train extractor with LLM hidden states.
647
-
648
- Args:
649
- unfreeze_layers: Number of top transformer layers to unfreeze (0 = fully frozen)
650
- extract_layer: Which layer to extract from (-1 = last)
651
- position_extract: Use position-specific extraction (legacy)
652
- digit_pred: Predict digits instead of bits (legacy)
653
- positional_digit: Use positional digit extraction (legacy, 100% on digits only)
654
- """
655
- hybrid = not (positional_digit or position_extract or digit_pred)
656
-
657
- print("=" * 70)
658
- print(" LLM TRAINING")
659
- if unfreeze_layers > 0:
660
- print(f" {unfreeze_layers} transformer layers unfrozen")
661
- else:
662
- print(" LLM frozen")
663
- if extract_layer != -1:
664
- print(f" Extracting from layer {extract_layer}")
665
- if hybrid:
666
- print(" HYBRID extraction (digit lookup + word learning)")
667
- elif positional_digit:
668
- print(" POSITIONAL DIGIT extraction (legacy, 100% on digits only)")
669
- elif position_extract:
670
- print(" Position-specific extraction (legacy)")
671
- elif digit_pred:
672
- print(" Digit-level prediction (legacy)")
673
- print("=" * 70)
674
-
675
- print("\nInitializing model...")
676
- model = ArithmeticModel(
677
- device=device,
678
- unfreeze_layers=unfreeze_layers,
679
- extract_layer=extract_layer,
680
- position_extract=position_extract,
681
- digit_pred=digit_pred,
682
- positional_digit=positional_digit,
683
- hybrid=hybrid
684
- )
685
-
686
- optimizer = optim.AdamW(model.trainable_parameters(), lr=lr)
687
- scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
688
-
689
- print(f"\nTraining config:")
690
- print(f" Epochs: {epochs}")
691
- print(f" Batch size: {batch_size}")
692
- print(f" Learning rate: {lr}")
693
- print(f" Unfreeze layers: {unfreeze_layers}")
694
- print(f" Samples/epoch: {batch_size * 20}")
695
-
696
- print(f"\nInitial evaluation (200 samples)...")
697
- acc, op_acc = evaluate_llm(model, 200)
698
- print(f" Accuracy: {acc:.4f}, Op accuracy: {op_acc:.4f}")
699
-
700
- print(f"\nStarting training...")
701
- print("-" * 70)
702
-
703
- best_acc = acc
704
- start_time = time.perf_counter()
705
-
706
- for epoch in range(epochs):
707
- model.extractor.train()
708
- if unfreeze_layers > 0:
709
- model.llm.train()
710
-
711
- max_val = get_curriculum_max(epoch, epochs)
712
-
713
- epoch_loss = 0
714
- if hybrid:
715
- epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0, 'n_words': 0, 'n_lookup': 0}
716
- elif positional_digit:
717
- epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0}
718
- else:
719
- epoch_losses = {'result': 0, 'a': 0, 'b': 0, 'op': 0}
720
- n_batches = 20
721
- epoch_start = time.perf_counter()
722
-
723
- for batch_idx in range(n_batches):
724
- batch_texts = []
725
- batch_a = []
726
- batch_b = []
727
- batch_op = []
728
- batch_result = []
729
- batch_a_values = []
730
- batch_b_values = []
731
-
732
- for _ in range(batch_size):
733
- text, a, b, op, result = generate_problem(max_val)
734
- batch_texts.append(text)
735
- batch_a.append(int_to_bits(a, device))
736
- batch_b.append(int_to_bits(b, device))
737
- batch_op.append(OPERATIONS.index(op))
738
- batch_result.append(int_to_bits(result, device))
739
- batch_a_values.append(a)
740
- batch_b_values.append(b)
741
-
742
- target_a = torch.stack(batch_a)
743
- target_b = torch.stack(batch_b)
744
- target_op = torch.tensor(batch_op, device=device)
745
- target_result = torch.stack(batch_result)
746
- target_a_values = torch.tensor(batch_a_values, device=device, dtype=torch.float32)
747
- target_b_values = torch.tensor(batch_b_values, device=device, dtype=torch.float32)
748
-
749
- optimizer.zero_grad()
750
-
751
- outputs = model(batch_texts)
752
- pred_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
753
-
754
- if hybrid:
755
- a_values, b_values, used_lookup = outputs[4], outputs[5], outputs[6]
756
- a_digit_logits, b_digit_logits = outputs[7], outputs[8]
757
- loss, losses = compute_hybrid_loss(
758
- pred_bits, op_logits, used_lookup,
759
- a_digit_logits, b_digit_logits,
760
- target_result, target_a_values, target_b_values, target_op, device
761
- )
762
- elif positional_digit:
763
- a_digit_logits_list = outputs[7]
764
- b_digit_logits_list = outputs[8]
765
- loss, losses = compute_positional_digit_loss(
766
- pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
767
- target_result, target_op, target_a_values, target_b_values, device
768
- )
769
- else:
770
- loss, losses = compute_llm_loss(
771
- pred_bits, a_bits, b_bits, op_logits,
772
- target_result, target_a, target_b, target_op
773
- )
774
-
775
- loss.backward()
776
- torch.nn.utils.clip_grad_norm_(model.trainable_parameters(), 1.0)
777
- optimizer.step()
778
-
779
- epoch_loss += loss.item()
780
- for k in epoch_losses:
781
- epoch_losses[k] += losses[k]
782
-
783
- if (batch_idx + 1) % 5 == 0:
784
- avg_so_far = epoch_loss / (batch_idx + 1)
785
- print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
786
-
787
- epoch_time = time.perf_counter() - epoch_start
788
- scheduler.step()
789
-
790
- avg_loss = epoch_loss / n_batches
791
- for k in epoch_losses:
792
- epoch_losses[k] /= n_batches
793
-
794
- acc, op_acc = evaluate_llm(model, 300)
795
- elapsed = time.perf_counter() - start_time
796
-
797
- marker = " *" if acc > best_acc else ""
798
- if acc > best_acc:
799
- best_acc = acc
800
-
801
- mem, _ = get_gpu_memory()
802
- print(f"Epoch {epoch+1:3d} | Loss: {avg_loss:.4f} | "
803
- f"Acc: {acc:.4f}{marker} | OpAcc: {op_acc:.4f} | "
804
- f"Range: 0-{max_val} | VRAM: {mem:.0f}MB | Time: {elapsed:.0f}s")
805
- if hybrid:
806
- print(f" Losses - result:{epoch_losses['result']:.4f} "
807
- f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
808
- f"op:{epoch_losses['op']:.4f} | words:{epoch_losses['n_words']:.0f} lookup:{epoch_losses['n_lookup']:.0f}")
809
- elif positional_digit:
810
- print(f" Losses - result:{epoch_losses['result']:.4f} "
811
- f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
812
- f"op:{epoch_losses['op']:.4f}")
813
- else:
814
- print(f" Losses - result:{epoch_losses['result']:.4f} "
815
- f"a:{epoch_losses['a']:.4f} b:{epoch_losses['b']:.4f} "
816
- f"op:{epoch_losses['op']:.4f}")
817
-
818
- if acc >= 0.99:
819
- print("\n" + "=" * 70)
820
- print(" TARGET ACHIEVED: 99%+ ACCURACY")
821
- print("=" * 70)
822
- break
823
-
824
- print("\n" + "=" * 70)
825
- print(" FINAL EVALUATION")
826
- print("=" * 70)
827
-
828
- acc, op_acc = evaluate_llm(model, 1000)
829
- print(f"Final accuracy: {acc:.4f}")
830
- print(f"Final op accuracy: {op_acc:.4f}")
831
- print(f"Best accuracy: {best_acc:.4f}")
832
-
833
- print("\nSample predictions:")
834
- for _ in range(10):
835
- text, a, b, op, expected = generate_problem()
836
- with torch.no_grad():
837
- outputs = model([text])
838
- result_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
839
- pred = bits_to_int(result_bits[0])
840
- pred_a = bits_to_int(a_bits[0])
841
- pred_b = bits_to_int(b_bits[0])
842
- pred_op = OPERATIONS[op_logits[0].argmax().item()]
843
-
844
- status = "OK" if pred == expected else "WRONG"
845
- print(f" '{text}' = {expected} | pred={pred} (a={pred_a}, b={pred_b}, op={pred_op}) [{status}]")
846
-
847
- save_path = "D:/8bit-threshold-computer/llm_integration/trained/llm.pt"
848
- save_dict = {
849
- 'extractor_state_dict': model.extractor.state_dict(),
850
- 'final_accuracy': acc,
851
- 'best_accuracy': best_acc,
852
- 'unfreeze_layers': unfreeze_layers,
853
- }
854
- if unfreeze_layers > 0:
855
- save_dict['llm_state_dict'] = {
856
- k: v for k, v in model.llm.state_dict().items()
857
- if any(f'layers.{i}.' in k for i in range(len(model.llm.model.layers) - unfreeze_layers, len(model.llm.model.layers)))
858
- }
859
- torch.save(save_dict, save_path)
860
- print(f"\nSaved to: {save_path}")
861
-
862
- return model, acc
863
-
864
-
865
- def main():
866
- parser = argparse.ArgumentParser(
867
- description='Unified training for threshold circuit LLM integration',
868
- formatter_class=argparse.RawDescriptionHelpFormatter,
869
- epilog="""
870
- Modes:
871
- router - Train only OpRouter with ground truth bits (sanity check)
872
- interface - Train BitEncoder + OpRouter with ground truth bits (sanity check)
873
- llm - Train extractor with LLM hidden states (the real training)
874
-
875
- LLM options:
876
- --unfreeze_layers N Fine-tune top N transformer layers
877
- --extract_layer N Extract from layer N (-1 = last)
878
- --position_extract Use position-specific extraction
879
- --digit_pred Predict digits instead of bits
880
-
881
- Baked-in: curriculum learning (0-9 -> 0-99 -> 0-255), 2x loss weight for a/b
882
-
883
- Examples:
884
- python train.py --mode llm --epochs 100
885
- python train.py --mode llm --position_extract
886
- python train.py --mode llm --digit_pred --extract_layer 12
887
- python train.py --mode llm --unfreeze_layers 4 --batch_size 4096
888
- """
889
- )
890
- parser.add_argument('--mode', type=str, required=True,
891
- choices=['router', 'interface', 'llm'],
892
- help='Training mode')
893
- parser.add_argument('--epochs', type=int, default=100, help='Number of epochs')
894
- parser.add_argument('--batch_size', type=int, default=512, help='Batch size (default: 512)')
895
- parser.add_argument('--lr', type=float, default=None,
896
- help='Learning rate (default: mode-specific)')
897
- parser.add_argument('--unfreeze_layers', type=int, default=0,
898
- help='Unfreeze top N transformer layers (default 0 = frozen)')
899
- parser.add_argument('--extract_layer', type=int, default=0,
900
- help='Which layer to extract from (default: 0 = embeddings, best for digits)')
901
- parser.add_argument('--position_extract', action='store_true',
902
- help='Use position-specific extraction (legacy)')
903
- parser.add_argument('--digit_pred', action='store_true',
904
- help='Predict digits instead of bits (legacy)')
905
- parser.add_argument('--positional_digit', action='store_true', default=False,
906
- help='Use positional digit extraction (legacy, 100%% on digits only)')
907
- parser.add_argument('--device', type=str, default='cuda', help='Device')
908
- args = parser.parse_args()
909
-
910
- torch.manual_seed(42)
911
- random.seed(42)
912
-
913
- if args.mode == 'router':
914
- lr = args.lr if args.lr is not None else 1e-2
915
- train_router(epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device)
916
-
917
- elif args.mode == 'interface':
918
- lr = args.lr if args.lr is not None else 1e-3
919
- model, fitness = train_interface(
920
- epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device
921
- )
922
-
923
- print("\n" + "=" * 70)
924
- print(" EXPERIMENT SUMMARY")
925
- print("=" * 70)
926
- print(f"\n Control (Vanilla SmolLM2-360M): 11.90%")
927
- print(f" Experimental (Trained Interface): {100*fitness:.2f}%")
928
- if fitness > 0:
929
- print(f"\n Improvement: {100*(fitness - 0.119)/0.119:.1f}%")
930
-
931
- if fitness >= 0.99:
932
- print("\n CONCLUSION: Frozen threshold circuits + trained interface")
933
- print(" achieves near-perfect arithmetic accuracy.")
934
- print(" Core thesis VALIDATED.")
935
- else:
936
- print(f"\n CONCLUSION: Further training or architecture changes needed.")
937
- print(f" Current gap: {100*(1.0 - fitness):.2f}%")
938
-
939
- elif args.mode == 'llm':
940
- lr = args.lr if args.lr is not None else 3e-4
941
- train_llm(
942
- epochs=args.epochs,
943
- batch_size=args.batch_size,
944
- lr=lr,
945
- unfreeze_layers=args.unfreeze_layers,
946
- extract_layer=args.extract_layer,
947
- position_extract=args.position_extract,
948
- digit_pred=args.digit_pred,
949
- positional_digit=args.positional_digit,
950
- device=args.device
951
- )
952
-
953
-
954
- if __name__ == "__main__":
955
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
paper/README.md CHANGED
@@ -6,51 +6,45 @@ Networks.* The manuscript and the records of the runs it reports.
6
  ```
7
  universal_construction_ternary.tex source
8
  universal_construction_ternary.pdf typeset
9
- runs/ the record written by each script
10
  ```
11
 
12
- ## What each script establishes
13
 
14
- Each script runs on its own and writes its record to `runs/`; `src/check_rocq.py`
15
- needs Rocq 9.1 and ocamlfind on the PATH.
 
16
 
17
-
18
- | Script | Record | Establishes |
19
  |---|---|---|
20
- | `src/host_netlist.py`, `src/host_family.py` | — | the step netlist of the host, a member of the constant-depth family H_A |
21
- | `src/check_host_family.py` | `paper_host_family.json` | one step of H_A against SUBLEQ_A for A = 3 to 12, on the evaluator in C |
22
- | `src/netlist_io.py` | — | the serialization sigma and its inverse |
23
- | `src/noise.py margin` | `paper_noise_margin.json` | the margin along the trajectory |
24
- | `src/noise.py profile` | `paper_noise_profile.json` | the pre-activation profile and its bound on the failure rate |
25
- | `src/paper_stats.py` | `paper_stats.json` | unit counts, levelization sizes, fan-in |
26
- | `src/paper_verify.py` | `paper_verify.json` | codec, framing, serialization, datapath, interpreter |
27
- | `src/paper_runs.py` | `paper_runs_*.json` | self-description, three generations, lockstep |
28
- | `src/check_codec.py` | `paper_codec.json` | the recipe codec at every boundary of the token format |
29
- | `src/check_optimal.py` | `paper_optimal.json` | the length bound is optimal on run-free strings |
30
- | `src/check_steps.py` | `paper_steps.json` | the closed forms for the step counts |
31
- | `src/check_device.py` | `paper_device.json` | the device against its definition |
32
- | `src/check_host.py` | `paper_host.json` | the reference machine against its definition |
33
- | `src/check_lev.py` | `paper_lev.json` | the levelization against gate-by-gate evaluation |
34
- | `src/check_framing.py` | `paper_framing.json` | `ser` injective, `inst` partial and inverse |
35
- | `src/check_ternarity.py` | `paper_ternarity.json` | no ternary unit computes the branch predicate |
36
- | `src/check_environment.py` | `paper_environment.json` | the cost of the environment against the cost of the network |
37
- | `src/check_sigma.py` | — | sigma is a function, injective, and inverted by the parser |
38
- | `src/check_family.py` | `paper_runs_family.json` | the constructor emits all 29 machines |
39
- | `src/check_family_gate.py` | `paper_runs_family_gate.json` | four of them on the netlist of sigma |
40
- | `src/check_c.py` | `paper_c_evaluator.json` | the host on the evaluator in C, `src/netlist_eval.c` |
41
- | `src/check_host_fold.py` | `paper_host_fold.json` | the folded host family, depth 11, against SUBLEQ_A for A = 3 to 12 |
42
- | `src/twohead.py` | `paper_twohead.json` | the translating organism on the two-head machine, three generations |
43
- | `src/twohead_net.py` | `paper_twohead_net.json` | the netlist N_{G,L} against the machine for windows of 64 and 4096 cells, its file, the organism on it in C, and the netlist extracted from Rocq |
44
- | `src/stored_host.py`, `src/stored_host_run.c` | `paper_stored_host.json` | the host compiled into records of the two-head machine: one pass against the host's step and its device, the image and its recipe, three generations in C |
45
- | `src/check_rocq.py` | `paper_rocq.json` | the Rocq development of `rocq/`: the netlist computes the step for every window, construction and self-reproduction by the organism |
46
- | `src/throughput.py` | `paper_throughput.json` | the rate of each evaluator |
47
- | `src/noise.py` | `paper_noise_*.json` | read noise, the deviation curve, weight error with leakage, the active-input counts of the orbit certificate |
48
- | `src/redundant.py` | `paper_redundant_*.json` | the amplified host N_3: one step against SUBLEQ on replicated states, the margin 3/2, exact self-reproduction in C, the run under noise, the deviation curve and its bound by layer, the failure rate of the state read by majority and the restoration bound, computed by `src/restore_bound.c` |
49
- | `src/neuromorphic.py` | `paper_neuromorphic.json` | the datapath on the Loihi and Akida neuron models |
50
- | `src/independent/*.py`, `nsim.c` | `independent_*.json` | separate code written from the paper's definitions: H_A rebuilt from Construction 3.8 for A = 3 to 12, Lev(N_host) from sigma, Tables 3 to 6 with a separate noise simulator, and the datapath on Loihi and Akida |
51
-
52
- `src/neuromorphic.py run` needs Lava, which requires Python 3.10; everything
53
- else runs in the environment of the repository.
54
 
55
  Rebuild the typeset paper with any TeX installation:
56
 
 
6
  ```
7
  universal_construction_ternary.tex source
8
  universal_construction_ternary.pdf typeset
9
+ runs/ the record written by each command
10
  ```
11
 
12
+ ## What each command establishes
13
 
14
+ Each command is `python src/<file>.py <command>` and writes its record to `runs/`;
15
+ `paper.py rocq` needs Rocq 9.1 and ocamlfind on the PATH, and `noise.py neuromorphic run`
16
+ needs Lava, which requires Python 3.10.
17
 
18
+ | Command | Record | Establishes |
 
19
  |---|---|---|
20
+ | `host.py` | — | the host family H_A, the serialization sigma and its inverse, the device, the constructor P and the program P*, and the evaluators |
21
+ | `paper.py host-family` | `paper_host_family.json` | one step of H_A against SUBLEQ_A for A = 3 to 12, on the evaluator in C |
22
+ | `paper.py host-fold` | `paper_host_fold.json` | the folded host family, depth 11, against SUBLEQ_A for A = 3 to 12 |
23
+ | `paper.py stats` | `paper_stats.json` | unit counts, levelization sizes, fan-in |
24
+ | `paper.py verify` | `paper_verify.json` | codec, framing, serialization, datapath, levelization |
25
+ | `paper.py runs` | `paper_runs_*.json` | self-description, three generations, lockstep |
26
+ | `paper.py codec` | `paper_codec.json` | the recipe codec at every boundary of the token format |
27
+ | `paper.py optimal` | `paper_optimal.json` | the length bound is optimal on run-free strings |
28
+ | `paper.py steps` | `paper_steps.json` | the closed forms for the step counts |
29
+ | `paper.py device` | `paper_device.json` | the device against its definition |
30
+ | `paper.py host` | `paper_host.json` | the reference machine against its definition |
31
+ | `paper.py lev` | `paper_lev.json` | the levelization against gate-by-gate evaluation |
32
+ | `paper.py framing` | `paper_framing.json` | `ser` injective, `inst` partial and inverse |
33
+ | `paper.py ternarity` | `paper_ternarity.json` | no ternary unit computes the branch predicate |
34
+ | `paper.py environment` | `paper_environment.json` | the cost of the environment against the cost of the network |
35
+ | `paper.py sigma` | — | sigma is a function, injective, and inverted by the parser |
36
+ | `paper.py family` | `paper_runs_family.json` | the constructor emits all 29 machines |
37
+ | `paper.py family-gate` | `paper_runs_family_gate.json` | four of them on the netlist of sigma |
38
+ | `paper.py c` | `paper_c_evaluator.json` | the host on the evaluator in C, `src/netlist_eval.c` |
39
+ | `paper.py throughput` | `paper_throughput.json` | the rate of each evaluator |
40
+ | `paper.py rocq` | `paper_rocq.json` | the Rocq development of `rocq/`, its assumptions and the netlist extracted from it |
41
+ | `twohead.py organism` | `paper_twohead.json` | the translating organism on the two-head machine, three generations |
42
+ | `twohead.py net` | `paper_twohead_net.json` | the netlist N_{G,L} against the machine for windows of 64 and 4096 cells, its file, the organism on it in C, and the netlist extracted from Rocq |
43
+ | `twohead.py stored` | `paper_stored_host.json` | the host compiled into records of the two-head machine: one pass against the host's step and its device, and three generations in C |
44
+ | `noise.py host` | `paper_noise_*.json` | the margin, the profile bound, read noise, the deviation curve, weight error with leakage, the active-input counts of the orbit certificate |
45
+ | `noise.py amplified` | `paper_redundant_*.json` | the amplified host N_3: one step on replicated states, the margin 3/2, exact self-reproduction in C, the run under noise, the deviation curve and its bound by layer, the failure rate of the state read by majority and the restoration bound, computed by `src/restore_bound.c` |
46
+ | `noise.py neuromorphic` | `paper_neuromorphic.json` | the datapath on the Loihi and Akida neuron models |
47
+ | `independent.py` | `independent_*.json` | code written from the paper's definitions alone: H_A rebuilt from Construction 3.8 for A = 3 to 12, Lev(N_host) from sigma, Tables 3 to 6 with the noise simulator `src/nsim.c`, and the datapath on Loihi and Akida |
 
 
 
 
 
 
48
 
49
  Rebuild the typeset paper with any TeX installation:
50
 
paper/universal_construction_ternary.pdf CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:29a8e26ce2577a85810fc815acc5eeaef1925cbaadb629aee163c64bf7cfdcf4
3
- size 364771
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf817d63746a8601c860f094ef25e7a746218a355e721248f44559caf8f3f441
3
+ size 364795
paper/universal_construction_ternary.tex CHANGED
@@ -2514,12 +2514,12 @@ $\sigma(\Nhost)$, and every generation of every run instantiated its evaluator
2514
  afresh from the bytes the previous generation emitted. The two-head machine was
2515
  evaluated by transcriptions of Definition~\ref{def:twohead} in Python and in C, by
2516
  the netlist $N^\alpha_{G,L}$ in Python and in C, and in the Rocq kernel. The
2517
- stored host is built from $\sigma(\Nhost)$ by \texttt{src/stored\_host.py}, which
2518
  also compares one pass with the host's step and the device, and its generations
2519
  are run by \texttt{src/stored\_host\_run.c}, a transcription of
2520
  Definition~\ref{def:twohead} that decodes the active bank once per change of
2521
  $\kappa$; the three generations of Remark~\ref{rem:storedcheck} take $9$ minutes.
2522
- Separate code in \texttt{src/independent/}, written from the definitions of this
2523
  paper, rebuilds $H_A$ from Construction~\ref{con:host} for $A=3$ to $12$,
2524
  levelizes $\sigma(\Nhost)$, repeats Tables~\ref{tab:curve} to~\ref{tab:bound3}
2525
  with a simulator in C and on the accelerator, and maps the datapath onto Loihi
@@ -2540,7 +2540,7 @@ of their length, and Theorem~\ref{thm:organism}. The file that defines the
2540
  organism and its description is written by \texttt{src/twohead.py}, and the
2541
  netlist definition, extracted to OCaml, writes for $N^6_{133,2930}$ and
2542
  $N^{12}_{5,40}$ files byte for byte equal to those that
2543
- \texttt{src/twohead\_net.py} writes.
2544
 
2545
  Besides the runs recorded in the remarks of Sections~\ref{sec:host}
2546
  to~\ref{sec:organism} and in Appendix~\ref{app:measure}, the following were
@@ -2637,7 +2637,7 @@ the Rocq definition. The records of the organism and of $S$ are laid out as in
2637
  Definition~\ref{def:twohead}, with the window cells of
2638
  Constructions~\ref{con:organism} and~\ref{con:stored}; within each block of
2639
  Construction~\ref{con:stored} the order of the records is that of
2640
- \texttt{src/stored\_host.py}, which gives each temporary the least cell free over
2641
  its live interval.
2642
  One generation of $\Omega^\ast$ takes $0.3$ seconds on~(a), $45$ seconds on~(d)
2643
  and $9$ minutes on~(b). All figures were
 
2514
  afresh from the bytes the previous generation emitted. The two-head machine was
2515
  evaluated by transcriptions of Definition~\ref{def:twohead} in Python and in C, by
2516
  the netlist $N^\alpha_{G,L}$ in Python and in C, and in the Rocq kernel. The
2517
+ stored host is built from $\sigma(\Nhost)$ by \texttt{src/twohead.py}, which
2518
  also compares one pass with the host's step and the device, and its generations
2519
  are run by \texttt{src/stored\_host\_run.c}, a transcription of
2520
  Definition~\ref{def:twohead} that decodes the active bank once per change of
2521
  $\kappa$; the three generations of Remark~\ref{rem:storedcheck} take $9$ minutes.
2522
+ Separate code in \texttt{src/independent.py}, written from the definitions of this
2523
  paper, rebuilds $H_A$ from Construction~\ref{con:host} for $A=3$ to $12$,
2524
  levelizes $\sigma(\Nhost)$, repeats Tables~\ref{tab:curve} to~\ref{tab:bound3}
2525
  with a simulator in C and on the accelerator, and maps the datapath onto Loihi
 
2540
  organism and its description is written by \texttt{src/twohead.py}, and the
2541
  netlist definition, extracted to OCaml, writes for $N^6_{133,2930}$ and
2542
  $N^{12}_{5,40}$ files byte for byte equal to those that
2543
+ \texttt{src/twohead.py} writes.
2544
 
2545
  Besides the runs recorded in the remarks of Sections~\ref{sec:host}
2546
  to~\ref{sec:organism} and in Appendix~\ref{app:measure}, the following were
 
2637
  Definition~\ref{def:twohead}, with the window cells of
2638
  Constructions~\ref{con:organism} and~\ref{con:stored}; within each block of
2639
  Construction~\ref{con:stored} the order of the records is that of
2640
+ \texttt{src/twohead.py}, which gives each temporary the least cell free over
2641
  its live interval.
2642
  One generation of $\Omega^\ast$ takes $0.3$ seconds on~(a), $45$ seconds on~(d)
2643
  and $9$ minutes on~(b). All figures were
src/attractor.py DELETED
@@ -1,261 +0,0 @@
1
- """Attractor computer: an energy-based threshold network.
2
-
3
- A Boolean circuit is compiled to a quadratic pseudo-Boolean energy
4
- E(s) = sum_i L[i] s_i + sum_{i<j} Q[i,j] s_i s_j over binary wire variables, with
5
- per-gate gadgets that are non-negative and zero exactly on the gate truth table:
6
-
7
- AND z=x&y : 3z + xy - 2xz - 2yz
8
- OR z=x|y : x + y + z + xy - 2xz - 2yz
9
- NOT z=~x : 1 - x - z + 2xz
10
-
11
- AND/OR/NOT are functionally complete, so any circuit compiles and its consistent
12
- assignment is the global minimum. There is no program counter or clock: the
13
- coupling matrix Q (with linear terms L) is the program, and execution is
14
- relaxation toward the minimum. The relaxation step is a threshold neuron over the
15
- same integer weights, s_i <- H(-(L[i] + sum_j Q[i,j] s_j)).
16
-
17
- The clamped wire subset selects the mode. Clamp inputs to evaluate forward (exact
18
- via topological propagation to the energy-0 fixed point); clamp outputs to invert
19
- (a multiplier run backward returns factors); clamp a CNF output to 1 to solve
20
- SAT. Forward evaluation is exact and linear in gate count; inversion and
21
- open-constraint solving are annealed ground-state search, NP-hard in general,
22
- with a zero-energy state certifying a correct assignment.
23
- """
24
- from __future__ import annotations
25
- import math
26
- import random
27
- from collections import defaultdict
28
- from typing import Dict, List, Optional, Tuple
29
-
30
-
31
- class Circuit:
32
- """Wire allocator and energy accumulator. Gates append exact QUBO gadgets
33
- and record the relation for topological forward evaluation."""
34
-
35
- def __init__(self) -> None:
36
- self.n = 0
37
- self.L: Dict[int, int] = defaultdict(int)
38
- self.Q: Dict[Tuple[int, int], int] = defaultdict(int)
39
- self.const = 0
40
- self.gates: List[Tuple[str, int, Tuple[int, ...]]] = []
41
-
42
- def wire(self) -> int:
43
- i = self.n
44
- self.n += 1
45
- return i
46
-
47
- def wires(self, k: int) -> List[int]:
48
- return [self.wire() for _ in range(k)]
49
-
50
- def _q(self, i: int, j: int, c: int) -> None:
51
- if i == j:
52
- self.L[i] += c
53
- else:
54
- self.Q[(min(i, j), max(i, j))] += c
55
-
56
- def AND(self, x: int, y: int) -> int:
57
- z = self.wire()
58
- self.L[z] += 3
59
- self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
60
- self.gates.append(("AND", z, (x, y)))
61
- return z
62
-
63
- def OR(self, x: int, y: int) -> int:
64
- z = self.wire()
65
- self.L[x] += 1; self.L[y] += 1; self.L[z] += 1
66
- self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
67
- self.gates.append(("OR", z, (x, y)))
68
- return z
69
-
70
- def NOT(self, x: int) -> int:
71
- z = self.wire()
72
- self.const += 1
73
- self.L[x] += -1; self.L[z] += -1
74
- self._q(x, z, 2)
75
- self.gates.append(("NOT", z, (x,)))
76
- return z
77
-
78
- def XOR(self, x: int, y: int) -> int:
79
- return self.OR(self.AND(x, self.NOT(y)), self.AND(self.NOT(x), y))
80
-
81
- def full_adder(self, x: int, y: int, cin: int) -> Tuple[int, int]:
82
- axy = self.XOR(x, y)
83
- s = self.XOR(axy, cin)
84
- cout = self.OR(self.AND(x, y), self.AND(cin, axy))
85
- return s, cout
86
-
87
- # ---- energy + couplings ------------------------------------------------
88
- def energy(self, s: List[int]) -> int:
89
- e = self.const
90
- for i, c in self.L.items():
91
- e += c * s[i]
92
- for (i, j), c in self.Q.items():
93
- e += c * s[i] * s[j]
94
- return e
95
-
96
- def neighbors(self) -> Dict[int, List[Tuple[int, int]]]:
97
- nbr: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
98
- for (i, j), c in self.Q.items():
99
- nbr[i].append((j, c))
100
- nbr[j].append((i, c))
101
- return nbr
102
-
103
- # ---- relaxation modes --------------------------------------------------
104
- def forward_eval(self, clamp: Dict[int, int]) -> List[int]:
105
- """Exact forward relaxation: propagate clamped inputs through the gate
106
- relations in topological order onto the energy-0 fixed point."""
107
- s = [0] * self.n
108
- for w, v in clamp.items():
109
- s[w] = v
110
- for op, z, ins in self.gates:
111
- if op == "AND":
112
- s[z] = s[ins[0]] & s[ins[1]]
113
- elif op == "OR":
114
- s[z] = s[ins[0]] | s[ins[1]]
115
- else:
116
- s[z] = 1 - s[ins[0]]
117
- return s
118
-
119
- def relax_energy(self, clamp: Dict[int, int], sweeps: int = 4000,
120
- t0: float = 4.0, t1: float = 0.02, seed: int = 0
121
- ) -> Tuple[List[int], bool]:
122
- """Canonical relaxation: anneal the full threshold network (every free
123
- wire), tracking the lowest-energy state. Universal but hard; the exact
124
- gadgets keep the target at energy 0."""
125
- nbr = self.neighbors()
126
- rng = random.Random(seed)
127
- s = [rng.randint(0, 1) for _ in range(self.n)]
128
- for w, v in clamp.items():
129
- s[w] = v
130
- free = [i for i in range(self.n) if i not in clamp]
131
- best, best_e = list(s), self.energy(s)
132
- for step in range(sweeps):
133
- T = t0 * (t1 / t0) ** (step / max(1, sweeps - 1))
134
- for _ in range(len(free)):
135
- i = free[rng.randrange(len(free))]
136
- field = self.L[i] + sum(c * s[j] for j, c in nbr[i])
137
- dE = (1 - 2 * s[i]) * field
138
- if dE <= 0 or rng.random() < math.exp(-dE / T):
139
- s[i] ^= 1
140
- e = self.energy(s)
141
- if e < best_e:
142
- best, best_e = list(s), e
143
- if best_e == 0:
144
- return best, True
145
- return best, best_e == 0
146
-
147
- def solve(self, free_inputs: List[int], fixed: Dict[int, int],
148
- target: Dict[int, int], sweeps: int = 3000, restarts: int = 80,
149
- seed: int = 0) -> Optional[List[int]]:
150
- """Open-constraint relaxation over a chosen set of driver wires, with
151
- the rest slaved through the circuit; anneal the output Hamming mismatch
152
- to zero. Clamp outputs and pass the inputs here to run backward."""
153
- rng = random.Random(seed)
154
-
155
- def mism(vals: Dict[int, int]) -> int:
156
- s = self.forward_eval({**fixed, **vals})
157
- return sum(1 for w, v in target.items() if s[w] != v)
158
-
159
- for _ in range(restarts):
160
- vals = {w: rng.randint(0, 1) for w in free_inputs}
161
- m = mism(vals)
162
- if m == 0:
163
- return self.forward_eval({**fixed, **vals})
164
- for step in range(sweeps):
165
- T = 2.0 * (0.02 / 2.0) ** (step / sweeps)
166
- w = free_inputs[rng.randrange(len(free_inputs))]
167
- vals[w] ^= 1
168
- m2 = mism(vals)
169
- if m2 <= m or rng.random() < math.exp(-(m2 - m) / T):
170
- m = m2
171
- if m == 0:
172
- return self.forward_eval({**fixed, **vals})
173
- else:
174
- vals[w] ^= 1
175
- return None
176
-
177
-
178
- # ---------------------------------------------------------------------------
179
- # Circuit builders
180
- # ---------------------------------------------------------------------------
181
- def adder(bits: int) -> Tuple[Circuit, dict]:
182
- c = Circuit()
183
- xs, ys = c.wires(bits), c.wires(bits)
184
- cin = c.wire()
185
- outs, carry = [], cin
186
- for k in range(bits):
187
- s, carry = c.full_adder(xs[k], ys[k], carry)
188
- outs.append(s)
189
- return c, {"xs": xs, "ys": ys, "cin": cin, "sum": outs + [carry]}
190
-
191
-
192
- def multiplier(bits: int) -> Tuple[Circuit, dict]:
193
- c = Circuit()
194
- xs, ys = c.wires(bits), c.wires(bits)
195
- zero = c.wire()
196
- acc = [zero] * (2 * bits)
197
- for i in range(bits):
198
- carry = zero
199
- for j in range(bits):
200
- acc[i + j], carry = c.full_adder(acc[i + j], c.AND(xs[i], ys[j]), carry)
201
- acc[i + bits] = carry
202
- return c, {"xs": xs, "ys": ys, "zero": zero, "prod": acc}
203
-
204
-
205
- _OPCODE = {"AND": 0, "OR": 1, "NOT": 2}
206
- _OPNAME = {v: k for k, v in _OPCODE.items()}
207
-
208
-
209
- def to_tensors(circ: Circuit, io: dict):
210
- """Serialize the coupling matrix (the program) and the gate list to tensors.
211
- Q is stored sparsely as index pairs and integer values."""
212
- import torch
213
- qi = sorted(circ.Q)
214
- q_idx = torch.tensor(qi if qi else [], dtype=torch.long).reshape(-1, 2)
215
- q_val = torch.tensor([circ.Q[k] for k in qi], dtype=torch.long)
216
- li = sorted(circ.L)
217
- l_idx = torch.tensor(li, dtype=torch.long)
218
- l_val = torch.tensor([circ.L[i] for i in li], dtype=torch.long)
219
- g_op = torch.tensor([_OPCODE[op] for op, _, _ in circ.gates], dtype=torch.long)
220
- g_out = torch.tensor([o for _, o, _ in circ.gates], dtype=torch.long)
221
- g_in = torch.tensor([[ins[0], ins[1] if len(ins) > 1 else -1]
222
- for _, _, ins in circ.gates], dtype=torch.long).reshape(-1, 2)
223
- t = {"Q_idx": q_idx, "Q_val": q_val, "L_idx": l_idx, "L_val": l_val,
224
- "gate_op": g_op, "gate_out": g_out, "gate_in": g_in}
225
- import json
226
- meta = {"machine": "attractor", "n": str(circ.n), "const": str(circ.const),
227
- "io": json.dumps({k: v for k, v in io.items()})}
228
- return t, meta
229
-
230
-
231
- def from_tensors(t: dict, meta: dict) -> Tuple[Circuit, dict]:
232
- import json
233
- c = Circuit()
234
- c.n = int(meta["n"])
235
- c.const = int(meta["const"])
236
- for (i, j), v in zip(t["Q_idx"].tolist(), t["Q_val"].tolist()):
237
- c.Q[(i, j)] = v
238
- for i, v in zip(t["L_idx"].tolist(), t["L_val"].tolist()):
239
- c.L[i] = v
240
- for op, out, ins in zip(t["gate_op"].tolist(), t["gate_out"].tolist(), t["gate_in"].tolist()):
241
- c.gates.append((_OPNAME[op], out, tuple(x for x in ins if x >= 0)))
242
- return c, json.loads(meta["io"])
243
-
244
-
245
- def cnf(clauses: List[List[int]], n_vars: int) -> Tuple[Circuit, dict]:
246
- """Compile a CNF formula. Literals are +v (var v) or -v (negation), v>=1.
247
- Returns the circuit, the variable wires, and the wire that is 1 iff the
248
- formula is satisfied. Clamp that wire to 1 and relax to find a model."""
249
- c = Circuit()
250
- var = {v: c.wire() for v in range(1, n_vars + 1)}
251
- clause_ws = []
252
- for cl in clauses:
253
- lits = [var[abs(l)] if l > 0 else c.NOT(var[abs(l)]) for l in cl]
254
- acc = lits[0]
255
- for w in lits[1:]:
256
- acc = c.OR(acc, w)
257
- clause_ws.append(acc)
258
- sat = clause_ws[0]
259
- for w in clause_ws[1:]:
260
- sat = c.AND(sat, w)
261
- return c, {"vars": var, "sat": sat}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/automata.py ADDED
@@ -0,0 +1,1759 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The attractor, reversible, cellular-automaton and tile machines, with their builders."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import math
6
+ import os
7
+ import random
8
+ import sys
9
+ from collections import defaultdict
10
+ from typing import Dict, List, Optional, Tuple
11
+
12
+ import torch
13
+ from safetensors import safe_open
14
+ from safetensors.torch import save_file, load_file
15
+
16
+ from machines import Net, compile_net, MatrixMachine
17
+
18
+
19
+ class Circuit:
20
+ """Wire allocator and energy accumulator."""
21
+
22
+ def __init__(self) -> None:
23
+ self.n = 0
24
+ self.L: Dict[int, int] = defaultdict(int)
25
+ self.Q: Dict[Tuple[int, int], int] = defaultdict(int)
26
+ self.const = 0
27
+ self.gates: List[Tuple[str, int, Tuple[int, ...]]] = []
28
+
29
+ def wire(self) -> int:
30
+ i = self.n
31
+ self.n += 1
32
+ return i
33
+
34
+ def wires(self, k: int) -> List[int]:
35
+ return [self.wire() for _ in range(k)]
36
+
37
+ def _q(self, i: int, j: int, c: int) -> None:
38
+ if i == j:
39
+ self.L[i] += c
40
+ else:
41
+ self.Q[(min(i, j), max(i, j))] += c
42
+
43
+ def AND(self, x: int, y: int) -> int:
44
+ z = self.wire()
45
+ self.L[z] += 3
46
+ self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
47
+ self.gates.append(("AND", z, (x, y)))
48
+ return z
49
+
50
+ def OR(self, x: int, y: int) -> int:
51
+ z = self.wire()
52
+ self.L[x] += 1; self.L[y] += 1; self.L[z] += 1
53
+ self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
54
+ self.gates.append(("OR", z, (x, y)))
55
+ return z
56
+
57
+ def NOT(self, x: int) -> int:
58
+ z = self.wire()
59
+ self.const += 1
60
+ self.L[x] += -1; self.L[z] += -1
61
+ self._q(x, z, 2)
62
+ self.gates.append(("NOT", z, (x,)))
63
+ return z
64
+
65
+ def XOR(self, x: int, y: int) -> int:
66
+ return self.OR(self.AND(x, self.NOT(y)), self.AND(self.NOT(x), y))
67
+
68
+ def full_adder(self, x: int, y: int, cin: int) -> Tuple[int, int]:
69
+ axy = self.XOR(x, y)
70
+ s = self.XOR(axy, cin)
71
+ cout = self.OR(self.AND(x, y), self.AND(cin, axy))
72
+ return s, cout
73
+
74
+ # ---- energy + couplings ------------------------------------------------
75
+ def energy(self, s: List[int]) -> int:
76
+ e = self.const
77
+ for i, c in self.L.items():
78
+ e += c * s[i]
79
+ for (i, j), c in self.Q.items():
80
+ e += c * s[i] * s[j]
81
+ return e
82
+
83
+ def neighbors(self) -> Dict[int, List[Tuple[int, int]]]:
84
+ nbr: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
85
+ for (i, j), c in self.Q.items():
86
+ nbr[i].append((j, c))
87
+ nbr[j].append((i, c))
88
+ return nbr
89
+
90
+ # ---- relaxation modes --------------------------------------------------
91
+ def forward_eval(self, clamp: Dict[int, int]) -> List[int]:
92
+ """Exact forward relaxation from the clamped inputs to the energy-0 fixed point."""
93
+ s = [0] * self.n
94
+ for w, v in clamp.items():
95
+ s[w] = v
96
+ for op, z, ins in self.gates:
97
+ if op == "AND":
98
+ s[z] = s[ins[0]] & s[ins[1]]
99
+ elif op == "OR":
100
+ s[z] = s[ins[0]] | s[ins[1]]
101
+ else:
102
+ s[z] = 1 - s[ins[0]]
103
+ return s
104
+
105
+ def relax_energy(self, clamp: Dict[int, int], sweeps: int = 4000,
106
+ t0: float = 4.0, t1: float = 0.02, seed: int = 0
107
+ ) -> Tuple[List[int], bool]:
108
+ """Anneal the full threshold network, tracking the lowest-energy state."""
109
+ nbr = self.neighbors()
110
+ rng = random.Random(seed)
111
+ s = [rng.randint(0, 1) for _ in range(self.n)]
112
+ for w, v in clamp.items():
113
+ s[w] = v
114
+ free = [i for i in range(self.n) if i not in clamp]
115
+ best, best_e = list(s), self.energy(s)
116
+ for step in range(sweeps):
117
+ T = t0 * (t1 / t0) ** (step / max(1, sweeps - 1))
118
+ for _ in range(len(free)):
119
+ i = free[rng.randrange(len(free))]
120
+ field = self.L[i] + sum(c * s[j] for j, c in nbr[i])
121
+ dE = (1 - 2 * s[i]) * field
122
+ if dE <= 0 or rng.random() < math.exp(-dE / T):
123
+ s[i] ^= 1
124
+ e = self.energy(s)
125
+ if e < best_e:
126
+ best, best_e = list(s), e
127
+ if best_e == 0:
128
+ return best, True
129
+ return best, best_e == 0
130
+
131
+ def solve(self, free_inputs: List[int], fixed: Dict[int, int],
132
+ target: Dict[int, int], sweeps: int = 3000, restarts: int = 80,
133
+ seed: int = 0) -> Optional[List[int]]:
134
+ """Anneal chosen driver wires until the outputs match, the other wires slaved through the circuit."""
135
+ rng = random.Random(seed)
136
+
137
+ def mism(vals: Dict[int, int]) -> int:
138
+ s = self.forward_eval({**fixed, **vals})
139
+ return sum(1 for w, v in target.items() if s[w] != v)
140
+
141
+ for _ in range(restarts):
142
+ vals = {w: rng.randint(0, 1) for w in free_inputs}
143
+ m = mism(vals)
144
+ if m == 0:
145
+ return self.forward_eval({**fixed, **vals})
146
+ for step in range(sweeps):
147
+ T = 2.0 * (0.02 / 2.0) ** (step / sweeps)
148
+ w = free_inputs[rng.randrange(len(free_inputs))]
149
+ vals[w] ^= 1
150
+ m2 = mism(vals)
151
+ if m2 <= m or rng.random() < math.exp(-(m2 - m) / T):
152
+ m = m2
153
+ if m == 0:
154
+ return self.forward_eval({**fixed, **vals})
155
+ else:
156
+ vals[w] ^= 1
157
+ return None
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Circuit builders
162
+ # ---------------------------------------------------------------------------
163
+ def adder(bits: int) -> Tuple[Circuit, dict]:
164
+ c = Circuit()
165
+ xs, ys = c.wires(bits), c.wires(bits)
166
+ cin = c.wire()
167
+ outs, carry = [], cin
168
+ for k in range(bits):
169
+ s, carry = c.full_adder(xs[k], ys[k], carry)
170
+ outs.append(s)
171
+ return c, {"xs": xs, "ys": ys, "cin": cin, "sum": outs + [carry]}
172
+
173
+
174
+ def multiplier(bits: int) -> Tuple[Circuit, dict]:
175
+ c = Circuit()
176
+ xs, ys = c.wires(bits), c.wires(bits)
177
+ zero = c.wire()
178
+ acc = [zero] * (2 * bits)
179
+ for i in range(bits):
180
+ carry = zero
181
+ for j in range(bits):
182
+ acc[i + j], carry = c.full_adder(acc[i + j], c.AND(xs[i], ys[j]), carry)
183
+ acc[i + bits] = carry
184
+ return c, {"xs": xs, "ys": ys, "zero": zero, "prod": acc}
185
+
186
+
187
+ _OPCODE = {"AND": 0, "OR": 1, "NOT": 2}
188
+
189
+ _OPNAME = {v: k for k, v in _OPCODE.items()}
190
+
191
+
192
+ def to_tensors(circ: Circuit, io: dict):
193
+ """Serialize the coupling matrix (the program) and the gate list to tensors."""
194
+ import torch
195
+ qi = sorted(circ.Q)
196
+ q_idx = torch.tensor(qi if qi else [], dtype=torch.long).reshape(-1, 2)
197
+ q_val = torch.tensor([circ.Q[k] for k in qi], dtype=torch.long)
198
+ li = sorted(circ.L)
199
+ l_idx = torch.tensor(li, dtype=torch.long)
200
+ l_val = torch.tensor([circ.L[i] for i in li], dtype=torch.long)
201
+ g_op = torch.tensor([_OPCODE[op] for op, _, _ in circ.gates], dtype=torch.long)
202
+ g_out = torch.tensor([o for _, o, _ in circ.gates], dtype=torch.long)
203
+ g_in = torch.tensor([[ins[0], ins[1] if len(ins) > 1 else -1]
204
+ for _, _, ins in circ.gates], dtype=torch.long).reshape(-1, 2)
205
+ t = {"Q_idx": q_idx, "Q_val": q_val, "L_idx": l_idx, "L_val": l_val,
206
+ "gate_op": g_op, "gate_out": g_out, "gate_in": g_in}
207
+ import json
208
+ meta = {"machine": "attractor", "n": str(circ.n), "const": str(circ.const),
209
+ "io": json.dumps({k: v for k, v in io.items()})}
210
+ return t, meta
211
+
212
+
213
+ def from_tensors(t: dict, meta: dict) -> Tuple[Circuit, dict]:
214
+ import json
215
+ c = Circuit()
216
+ c.n = int(meta["n"])
217
+ c.const = int(meta["const"])
218
+ for (i, j), v in zip(t["Q_idx"].tolist(), t["Q_val"].tolist()):
219
+ c.Q[(i, j)] = v
220
+ for i, v in zip(t["L_idx"].tolist(), t["L_val"].tolist()):
221
+ c.L[i] = v
222
+ for op, out, ins in zip(t["gate_op"].tolist(), t["gate_out"].tolist(), t["gate_in"].tolist()):
223
+ c.gates.append((_OPNAME[op], out, tuple(x for x in ins if x >= 0)))
224
+ return c, json.loads(meta["io"])
225
+
226
+
227
+ def cnf(clauses: List[List[int]], n_vars: int) -> Tuple[Circuit, dict]:
228
+ """Compile a CNF formula."""
229
+ c = Circuit()
230
+ var = {v: c.wire() for v in range(1, n_vars + 1)}
231
+ clause_ws = []
232
+ for cl in clauses:
233
+ lits = [var[abs(l)] if l > 0 else c.NOT(var[abs(l)]) for l in cl]
234
+ acc = lits[0]
235
+ for w in lits[1:]:
236
+ acc = c.OR(acc, w)
237
+ clause_ws.append(acc)
238
+ sat = clause_ws[0]
239
+ for w in clause_ws[1:]:
240
+ sat = c.AND(sat, w)
241
+ return c, {"vars": var, "sat": sat}
242
+
243
+
244
+ def test_forward():
245
+ ok = True
246
+ for bits in (4, 8):
247
+ c, io = adder(bits)
248
+ rng = random.Random(bits)
249
+ bad = 0
250
+ for _ in range(300):
251
+ a, b = rng.randint(0, (1 << bits) - 1), rng.randint(0, (1 << bits) - 1)
252
+ clamp = {io["cin"]: 0}
253
+ for k in range(bits):
254
+ clamp[io["xs"][k]] = (a >> k) & 1
255
+ clamp[io["ys"][k]] = (b >> k) & 1
256
+ s = c.forward_eval(clamp)
257
+ got = sum(s[w] << k for k, w in enumerate(io["sum"]))
258
+ if got != a + b or c.energy(s) != 0:
259
+ bad += 1
260
+ print(f" forward adder {bits}-bit: {'OK' if bad == 0 else f'FAIL({bad})'}")
261
+ ok &= bad == 0
262
+ for bits in (3, 5):
263
+ c, io = multiplier(bits)
264
+ rng = random.Random(100 + bits)
265
+ bad = 0
266
+ for _ in range(300):
267
+ a, b = rng.randint(0, (1 << bits) - 1), rng.randint(0, (1 << bits) - 1)
268
+ clamp = {io["zero"]: 0}
269
+ for k in range(bits):
270
+ clamp[io["xs"][k]] = (a >> k) & 1
271
+ clamp[io["ys"][k]] = (b >> k) & 1
272
+ s = c.forward_eval(clamp)
273
+ got = sum(s[w] << k for k, w in enumerate(io["prod"]))
274
+ if got != a * b or c.energy(s) != 0:
275
+ bad += 1
276
+ print(f" forward multiplier {bits}-bit: {'OK' if bad == 0 else f'FAIL({bad})'}")
277
+ ok &= bad == 0
278
+ return ok
279
+
280
+
281
+ def test_energy_relax():
282
+ """The canonical form: anneal the whole network (no propagation shortcut)."""
283
+ c, io = adder(4)
284
+ rng = random.Random(3)
285
+ bad = 0
286
+ for _ in range(20):
287
+ a, b = rng.randint(0, 15), rng.randint(0, 15)
288
+ clamp = {io["cin"]: 0}
289
+ for k in range(4):
290
+ clamp[io["xs"][k]] = (a >> k) & 1
291
+ clamp[io["ys"][k]] = (b >> k) & 1
292
+ conv = False
293
+ for attempt in range(4): # annealers restart
294
+ s, conv = c.relax_energy(clamp, sweeps=6000, seed=rng.randint(0, 1 << 30))
295
+ got = sum(s[w] << k for k, w in enumerate(io["sum"]))
296
+ if conv and got == a + b:
297
+ break
298
+ if not conv:
299
+ bad += 1
300
+ print(f" whole-network energy relaxation (4-bit adder, 20 cases): "
301
+ f"{'OK' if bad == 0 else f'reached ground state in {20 - bad}/20'}")
302
+ return bad == 0
303
+
304
+
305
+ def test_factor():
306
+ ok = True
307
+ for bits, targets in ((4, [15, 35, 143]), (5, [21, 55, 91])):
308
+ c, io = multiplier(bits)
309
+ for N in targets:
310
+ target = {io["prod"][k]: (N >> k) & 1 for k in range(2 * bits)}
311
+ s = c.solve(io["xs"] + io["ys"], {io["zero"]: 0}, target, seed=N)
312
+ if s is None:
313
+ print(f" factor {N}: not found")
314
+ ok = False
315
+ continue
316
+ a = sum(s[io["xs"][k]] << k for k in range(bits))
317
+ b = sum(s[io["ys"][k]] << k for k in range(bits))
318
+ good = a * b == N and 1 < a < N and 1 < b < N
319
+ print(f" factor {N} ({bits}x{bits}): {a} x {b} {'OK' if a * b == N else 'WRONG'}")
320
+ ok &= a * b == N
321
+ return ok
322
+
323
+
324
+ def test_sat():
325
+ # (x1 | x2 | ~x3) & (~x1 | x3) & (x2 | x3) & (~x2 | ~x3), a satisfiable 3-SAT.
326
+ clauses = [[1, 2, -3], [-1, 3], [2, 3], [-2, -3]]
327
+ c, io = cnf(clauses, 3)
328
+ s = c.solve(list(io["vars"].values()), {}, {io["sat"]: 1}, seed=1)
329
+ if s is None:
330
+ print(" SAT solve: no model found")
331
+ return False
332
+ assign = {v: s[w] for v, w in io["vars"].items()}
333
+ sat = all(any((assign[abs(l)] == 1) if l > 0 else (assign[abs(l)] == 0) for l in cl)
334
+ for cl in clauses)
335
+ print(f" SAT solve: model {assign} {'satisfies' if sat else 'FAILS'} the formula")
336
+ return sat
337
+
338
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
339
+
340
+
341
+ ATTRACTOR_OUT = os.path.join(ROOT, "variants", "neural_attractor.safetensors")
342
+
343
+ BITS = 8
344
+
345
+
346
+ def main_build_attractor() -> int:
347
+ c, io = multiplier(BITS)
348
+ tensors, meta = to_tensors(c, io)
349
+ save_file(tensors, ATTRACTOR_OUT, metadata=meta)
350
+ size = os.path.getsize(ATTRACTOR_OUT)
351
+ print(f"Built {os.path.relpath(ATTRACTOR_OUT, ROOT)}: {BITS}x{BITS} multiplier as couplings")
352
+ print(f" wires={c.n} Q entries={len(c.Q)} L entries={len(c.L)} size={size/1024:.1f} KB")
353
+
354
+ # round-trip
355
+ t = load_file(ATTRACTOR_OUT)
356
+ with safe_open(ATTRACTOR_OUT, framework="pt") as f:
357
+ m = f.metadata()
358
+ c2, io2 = from_tensors(t, m)
359
+ xs, ys, zero, prod = io2["xs"], io2["ys"], io2["zero"], io2["prod"]
360
+
361
+ rng = random.Random(0)
362
+ bad = 0
363
+ for _ in range(200):
364
+ a, b = rng.randint(0, 255), rng.randint(0, 255)
365
+ clamp = {zero: 0}
366
+ for k in range(BITS):
367
+ clamp[xs[k]] = (a >> k) & 1
368
+ clamp[ys[k]] = (b >> k) & 1
369
+ s = c2.forward_eval(clamp)
370
+ got = sum(s[w] << k for k, w in enumerate(prod))
371
+ if got != a * b or c2.energy(s) != 0:
372
+ bad += 1
373
+ print(f" round-trip forward multiply (200 cases): {'OK' if bad == 0 else f'FAIL({bad})'}")
374
+
375
+ # backward: factor a small product through the loaded couplings
376
+ N = 35
377
+ target = {prod[k]: (N >> k) & 1 for k in range(2 * BITS)}
378
+ s = c2.solve(xs + ys, {zero: 0}, target, sweeps=2500, restarts=120, seed=N)
379
+ if s is not None:
380
+ fa = sum(s[xs[k]] << k for k in range(BITS))
381
+ fb = sum(s[ys[k]] << k for k in range(BITS))
382
+ print(f" round-trip backward factor {N}: {fa} x {fb} {'OK' if fa * fb == N else 'WRONG'}")
383
+ ok = fa * fb == N
384
+ else:
385
+ print(f" round-trip backward factor {N}: not found")
386
+ ok = False
387
+ return 0 if (bad == 0 and ok) else 1
388
+
389
+ # --- threshold-gate truth: the target updates are computed by Heaviside gates ---
390
+ def H(x: int) -> int:
391
+ return 1 if x >= 0 else 0
392
+
393
+
394
+ def g_and(a: int, b: int) -> int:
395
+ return H(a + b - 2) # fires iff a=b=1
396
+
397
+
398
+ def g_xor(a: int, b: int) -> int:
399
+ # XOR as AND(OR, NAND): OR=H(a+b-1), NAND=H(1-a-b), out=AND(OR,NAND)
400
+ return g_and(H(a + b - 1), H(1 - a - b))
401
+
402
+
403
+ # --- reversible primitives, in place on a bit register (a Python list) ---
404
+ def NOT(reg: List[int], t: int) -> None:
405
+ reg[t] = 1 - reg[t]
406
+
407
+
408
+ def CNOT(reg: List[int], c: int, t: int) -> None:
409
+ reg[t] = g_xor(reg[t], reg[c])
410
+
411
+
412
+ def TOFF(reg: List[int], a: int, b: int, t: int) -> None:
413
+ reg[t] = g_xor(reg[t], g_and(reg[a], reg[b]))
414
+
415
+
416
+ def FRED(reg: List[int], c: int, x: int, y: int) -> None:
417
+ # controlled swap of x,y on control c
418
+ if reg[c]:
419
+ reg[x], reg[y] = reg[y], reg[x]
420
+
421
+
422
+ # --- Cuccaro ripple-carry adder: b += a in place, one carry ancilla ---
423
+ # MAJ(c,b,a): b^=a ; c^=a ; a ^= b&c UMA(c,b,a): a ^= b&c ; c^=a ; b^=c
424
+ def _maj(reg, c, b, a):
425
+ CNOT(reg, a, b)
426
+ CNOT(reg, a, c)
427
+ TOFF(reg, b, c, a)
428
+
429
+
430
+ def _uma(reg, c, b, a):
431
+ TOFF(reg, b, c, a)
432
+ CNOT(reg, a, c)
433
+ CNOT(reg, c, b)
434
+
435
+
436
+ # Each primitive below is its own inverse, so the inverse of a gate sequence is
437
+ # the reversed sequence. Building the adder as an op list gives subtraction for
438
+ # free (run it backward).
439
+ def _maj_ops(c, b, a):
440
+ return [(CNOT, a, b), (CNOT, a, c), (TOFF, b, c, a)]
441
+
442
+
443
+ def _uma_ops(c, b, a):
444
+ return [(TOFF, b, c, a), (CNOT, a, c), (CNOT, c, b)]
445
+
446
+
447
+ def _adder_ops(a_bits, b_bits, carry, cout=None):
448
+ n = len(a_bits)
449
+ ops = _maj_ops(carry, b_bits[0], a_bits[0])
450
+ for i in range(1, n):
451
+ ops += _maj_ops(a_bits[i - 1], b_bits[i], a_bits[i])
452
+ if cout is not None:
453
+ ops.append((CNOT, a_bits[n - 1], cout))
454
+ for i in range(n - 1, 0, -1):
455
+ ops += _uma_ops(a_bits[i - 1], b_bits[i], a_bits[i])
456
+ ops += _uma_ops(carry, b_bits[0], a_bits[0])
457
+ return ops
458
+
459
+
460
+ def _apply(reg, ops, inverse=False):
461
+ for gate, *args in (reversed(ops) if inverse else ops):
462
+ gate(reg, *args)
463
+
464
+
465
+ def add_into(reg, a_bits, b_bits, carry, cout=None):
466
+ """b <- (a + b) mod 2^n (LSB first); a and carry (=0) restored."""
467
+ _apply(reg, _adder_ops(a_bits, b_bits, carry, cout))
468
+
469
+
470
+ def sub_into(reg, a_bits, b_bits, carry, cout=None):
471
+ """b <- (b - a) mod 2^n: the adder run backward."""
472
+ _apply(reg, _adder_ops(a_bits, b_bits, carry, cout), inverse=True)
473
+
474
+
475
+ def xor_into(reg, a_bits, b_bits):
476
+ """b <- b XOR a, bitwise (self-inverse)."""
477
+ for a, b in zip(a_bits, b_bits):
478
+ CNOT(reg, a, b)
479
+
480
+
481
+ def incr(reg, b_bits, one_bits, carry):
482
+ """b <- b + 1 mod 2^n."""
483
+ add_into(reg, one_bits, b_bits, carry)
484
+
485
+
486
+ def neg_into(reg, b_bits, one_bits, carry):
487
+ """b <- (-b) mod 2^n via two's complement (~b then +1). Self-inverse."""
488
+ for t in b_bits:
489
+ NOT(reg, t)
490
+ add_into(reg, one_bits, b_bits, carry)
491
+
492
+
493
+ def rot_left(reg, b_bits, k=1):
494
+ """Rotate the word left by k (a permutation of bit positions; reversible)."""
495
+ n = len(b_bits)
496
+ k %= n
497
+ vals = [reg[b_bits[i]] for i in range(n)]
498
+ for i in range(n):
499
+ reg[b_bits[(i + k) % n]] = vals[i]
500
+
501
+
502
+ def and_into(reg, a_bits, b_bits, t_bits):
503
+ """t <- t XOR (a AND b), bitwise (word-level Toffoli). Self-inverse."""
504
+ for a, b, t in zip(a_bits, b_bits, t_bits):
505
+ TOFF(reg, a, b, t)
506
+
507
+
508
+ # --- verification helpers ---
509
+ def is_permutation(fn, nbits: int) -> bool:
510
+ """Check fn: {0,1}^nbits -> {0,1}^nbits is a bijection (exhaustive)."""
511
+ seen = set()
512
+ for x in range(1 << nbits):
513
+ reg = [(x >> k) & 1 for k in range(nbits)]
514
+ fn(reg)
515
+ y = sum(b << k for k, b in enumerate(reg))
516
+ seen.add(y)
517
+ return len(seen) == (1 << nbits)
518
+
519
+
520
+ def _test_primitives():
521
+ ok = True
522
+ ok &= is_permutation(lambda r: NOT(r, 0), 1)
523
+ ok &= is_permutation(lambda r: CNOT(r, 0, 1), 2)
524
+ ok &= is_permutation(lambda r: TOFF(r, 0, 1, 2), 3)
525
+ ok &= is_permutation(lambda r: FRED(r, 0, 1, 2), 3)
526
+ print(f" primitives bijective (NOT/CNOT/TOFF/FRED): {'OK' if ok else 'FAIL'}")
527
+ return ok
528
+
529
+
530
+ def _test_adder(width=4):
531
+ # layout: a[0..w-1], b[0..w-1], carry
532
+ a_bits = list(range(width))
533
+ b_bits = list(range(width, 2 * width))
534
+ carry = 2 * width
535
+ n = 2 * width + 1
536
+ ok_perm = is_permutation(lambda r: add_into(r, a_bits, b_bits, carry), n)
537
+ bad = 0
538
+ mask = (1 << width) - 1
539
+ for a in range(1 << width):
540
+ for b in range(1 << width):
541
+ reg = [0] * n
542
+ for k in range(width):
543
+ reg[a_bits[k]] = (a >> k) & 1
544
+ reg[b_bits[k]] = (b >> k) & 1
545
+ add_into(reg, a_bits, b_bits, carry)
546
+ got_a = sum(reg[a_bits[k]] << k for k in range(width))
547
+ got_b = sum(reg[b_bits[k]] << k for k in range(width))
548
+ if got_a != a or got_b != ((a + b) & mask) or reg[carry] != 0:
549
+ bad += 1
550
+ print(f" Cuccaro adder {width}-bit: bijection={'OK' if ok_perm else 'FAIL'} "
551
+ f"b<-a+b, a & carry restored={'OK' if bad == 0 else f'FAIL({bad})'}")
552
+ return ok_perm and bad == 0
553
+
554
+
555
+ def bennett(reg, a_b, b_b, c_b, scr_b, out_b, carry):
556
+ """Bennett compute-copy-uncompute for the irreversible f(a,b,c)=(a+b) XOR c."""
557
+ ops = []
558
+ # compute f into scratch (scratch starts 0): scratch += a; scratch += b; scratch ^= c
559
+ ops += _adder_ops(a_b, scr_b, carry)
560
+ ops += _adder_ops(b_b, scr_b, carry)
561
+ ops += [(CNOT, c, s) for c, s in zip(c_b, scr_b)]
562
+ # copy scratch -> out
563
+ ops += [(CNOT, s, o) for s, o in zip(scr_b, out_b)]
564
+ # uncompute scratch (reverse of the compute prefix)
565
+ n_comp = len(_adder_ops(a_b, scr_b, carry)) * 2 + len(c_b)
566
+ ops += [(g, *args) for g, *args in reversed(ops[:n_comp])]
567
+ _apply(reg, ops)
568
+ return ops
569
+
570
+
571
+ def _test_bennett(width=4):
572
+ a_b = list(range(width))
573
+ b_b = list(range(width, 2 * width))
574
+ c_b = list(range(2 * width, 3 * width))
575
+ scr_b = list(range(3 * width, 4 * width))
576
+ out_b = list(range(4 * width, 5 * width))
577
+ carry = 5 * width
578
+ n = 5 * width + 1
579
+ mask = (1 << width) - 1
580
+ bad = 0
581
+ for a in range(1 << width):
582
+ for b in range(1 << width):
583
+ for c in range(1 << width):
584
+ r = [0] * n
585
+ for k in range(width):
586
+ r[a_b[k]] = (a >> k) & 1
587
+ r[b_b[k]] = (b >> k) & 1
588
+ r[c_b[k]] = (c >> k) & 1
589
+ bennett(r, a_b, b_b, c_b, scr_b, out_b, carry)
590
+ ra = sum(r[a_b[k]] << k for k in range(width))
591
+ rb = sum(r[b_b[k]] << k for k in range(width))
592
+ rc = sum(r[c_b[k]] << k for k in range(width))
593
+ rs = sum(r[scr_b[k]] << k for k in range(width))
594
+ ro = sum(r[out_b[k]] << k for k in range(width))
595
+ if (ra, rb, rc, rs, ro, r[carry]) != (a, b, c, 0, ((a + b) & mask) ^ c, 0):
596
+ bad += 1
597
+ print(f" Bennett (a,b,c,0,0)->(a,b,c,0,(a+b)^c), scratch cleaned "
598
+ f"[{width}-bit]: {'OK' if bad == 0 else f'FAIL({bad})'}")
599
+ return bad == 0
600
+
601
+
602
+ def _test_alu(width=4):
603
+ mask = (1 << width) - 1
604
+ a_b = list(range(width))
605
+ b_b = list(range(width, 2 * width))
606
+ one_b = list(range(2 * width, 3 * width))
607
+ t_b = list(range(3 * width, 4 * width))
608
+ carry = 4 * width
609
+ n = 4 * width + 1
610
+
611
+ def fresh(a=0, b=0, t=0, one=False):
612
+ r = [0] * n
613
+ for k in range(width):
614
+ r[a_b[k]] = (a >> k) & 1
615
+ r[b_b[k]] = (b >> k) & 1
616
+ r[t_b[k]] = (t >> k) & 1
617
+ if one:
618
+ r[one_b[0]] = 1
619
+ return r
620
+
621
+ def rd(r, bits):
622
+ return sum(r[bits[k]] << k for k in range(width))
623
+
624
+ results = {}
625
+ # subtract
626
+ bad = 0
627
+ for a in range(1 << width):
628
+ for b in range(1 << width):
629
+ r = fresh(a, b)
630
+ sub_into(r, a_b, b_b, carry)
631
+ if rd(r, b_b) != ((b - a) & mask) or rd(r, a_b) != a or r[carry] != 0:
632
+ bad += 1
633
+ results["sub b-=a"] = bad == 0 and is_permutation(lambda r: sub_into(r, a_b, b_b, carry), n)
634
+ # xor
635
+ bad = 0
636
+ for a in range(1 << width):
637
+ for b in range(1 << width):
638
+ r = fresh(a, b)
639
+ xor_into(r, a_b, b_b)
640
+ if rd(r, b_b) != (a ^ b) or rd(r, a_b) != a:
641
+ bad += 1
642
+ results["xor b^=a"] = bad == 0
643
+ # negate
644
+ bad = 0
645
+ for b in range(1 << width):
646
+ r = fresh(b=b, one=True)
647
+ neg_into(r, b_b, one_b, carry)
648
+ if rd(r, b_b) != ((-b) & mask) or rd(r, one_b) != 1 or r[carry] != 0:
649
+ bad += 1
650
+ results["neg b=-b"] = bad == 0
651
+ # increment
652
+ bad = 0
653
+ for b in range(1 << width):
654
+ r = fresh(b=b, one=True)
655
+ incr(r, b_b, one_b, carry)
656
+ if rd(r, b_b) != ((b + 1) & mask):
657
+ bad += 1
658
+ results["incr b+=1"] = bad == 0
659
+ # rotate
660
+ bad = 0
661
+ for b in range(1 << width):
662
+ r = fresh(b=b)
663
+ rot_left(r, b_b, 1)
664
+ exp = ((b << 1) | (b >> (width - 1))) & mask
665
+ if rd(r, b_b) != exp:
666
+ bad += 1
667
+ results["rot_left"] = bad == 0
668
+ # and-into (Toffoli word)
669
+ bad = 0
670
+ for a in range(1 << width):
671
+ for b in range(1 << width):
672
+ for t in range(1 << width):
673
+ r = fresh(a, b, t)
674
+ and_into(r, a_b, b_b, t_b)
675
+ if rd(r, t_b) != (t ^ (a & b)):
676
+ bad += 1
677
+ results["and t^=a&b"] = bad == 0
678
+ ok = all(results.values())
679
+ print(" reversible ALU " + f"{width}-bit: " +
680
+ " ".join(f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items()))
681
+ return ok
682
+
683
+
684
+ class RCPU:
685
+ def __init__(self, program: List[tuple], k_regs=4, width=8, mem_words=16):
686
+ self.prog = program
687
+ self.K = k_regs
688
+ self.W = width
689
+ self.M = mem_words
690
+ self.mask = (1 << width) - 1
691
+ self.L = len(program)
692
+
693
+ def new_state(self, regs=None, mem=None) -> dict:
694
+ return {"R": list(regs) + [0] * (self.K - len(regs)) if regs else [0] * self.K,
695
+ "PC": 0, "BR": 1, "MEM": list(mem) + [0] * (self.M - len(mem)) if mem else [0] * self.M}
696
+
697
+ # ---- reversible instruction effects (forward and inverse) ----
698
+ def _data(self, s, I, inverse: bool):
699
+ R, MEM, m = s["R"], s["MEM"], self.mask
700
+ op = I[0]
701
+ if op == "ADD":
702
+ d, r = I[1], I[2]
703
+ R[d] = (R[d] - R[r]) & m if inverse else (R[d] + R[r]) & m
704
+ elif op == "SUB":
705
+ d, r = I[1], I[2]
706
+ R[d] = (R[d] + R[r]) & m if inverse else (R[d] - R[r]) & m
707
+ elif op == "ADDI":
708
+ d, k = I[1], I[2]
709
+ R[d] = (R[d] - k) & m if inverse else (R[d] + k) & m
710
+ elif op == "XOR":
711
+ R[I[1]] ^= R[I[2]]
712
+ elif op == "XORI":
713
+ R[I[1]] ^= (I[2] & m)
714
+ elif op == "NEG":
715
+ R[I[1]] = (-R[I[1]]) & m
716
+ elif op == "TOFF":
717
+ R[I[1]] ^= (R[I[2]] & R[I[3]])
718
+ elif op == "ROL":
719
+ k = (-I[2] if inverse else I[2]) % self.W
720
+ R[I[1]] = ((R[I[1]] << k) | (R[I[1]] >> (self.W - k))) & m if k else R[I[1]]
721
+ elif op == "EXCH":
722
+ d, r = I[1], I[2]
723
+ a = R[r] % self.M
724
+ R[d], MEM[a] = MEM[a], R[d]
725
+ # BRA/BEZ/HALT have no data effect
726
+
727
+ def _toggle(self, s, I):
728
+ """Reversible control: toggle BR for a taken branch (self-inverse)."""
729
+ op = I[0]
730
+ if op == "BRA":
731
+ s["BR"] ^= (I[1] ^ 1)
732
+ elif op == "BEZ":
733
+ if s["R"][I[1]] == 0:
734
+ s["BR"] ^= (I[2] ^ 1)
735
+
736
+ # ---- single-step transition and its inverse ----
737
+ def step(self, s):
738
+ I = self.prog[s["PC"]]
739
+ self._data(s, I, inverse=False)
740
+ self._toggle(s, I)
741
+ s["PC"] = (s["PC"] + s["BR"]) % self.L
742
+
743
+ def step_back(self, s):
744
+ s["PC"] = (s["PC"] - s["BR"]) % self.L
745
+ I = self.prog[s["PC"]]
746
+ self._toggle(s, I) # self-inverse: restores BR
747
+ self._data(s, I, inverse=True)
748
+
749
+ def run(self, s, steps):
750
+ for _ in range(steps):
751
+ self.step(s)
752
+ return s
753
+
754
+ def run_back(self, s, steps):
755
+ for _ in range(steps):
756
+ self.step_back(s)
757
+ return s
758
+
759
+
760
+ def _clone(s):
761
+ return {"R": list(s["R"]), "PC": s["PC"], "BR": s["BR"], "MEM": list(s["MEM"])}
762
+
763
+
764
+ def _eq(a, b):
765
+ return a["R"] == b["R"] and a["PC"] == b["PC"] and a["BR"] == b["BR"] and a["MEM"] == b["MEM"]
766
+
767
+
768
+ def test_straight_line():
769
+ prog = [("ADD", 1, 0), ("XOR", 1, 0), ("NEG", 1), ("ADDI", 1, 7),
770
+ ("TOFF", 2, 0, 1), ("ROL", 0, 1)]
771
+ m = RCPU(prog, width=8)
772
+ ok = True
773
+ for a in (5, 0, 255, 100):
774
+ for b in (3, 1, 200):
775
+ s0 = m.new_state([a, b, 0, 0])
776
+ s = _clone(s0)
777
+ m.run(s, len(prog))
778
+ fwd = _clone(s)
779
+ m.run_back(s, len(prog))
780
+ ok &= _eq(s, s0) # round-trip recovers the exact input
781
+ print(f" straight-line round-trip (dir -1 recovers input): {'OK' if ok else 'FAIL'}")
782
+ return ok
783
+
784
+
785
+ def test_bijection():
786
+ """The single-step transition is a bijection: step_back inverts step on every instruction."""
787
+ import itertools
788
+ W = 4
789
+ prog = [
790
+ ("ADD", 1, 0), ("SUB", 1, 0), ("ADDI", 2, 3), ("XOR", 1, 2),
791
+ ("XORI", 0, 5), ("NEG", 3), ("TOFF", 3, 0, 1), ("ROL", 0, 1),
792
+ ("EXCH", 2, 3), ("BRA", 2), ("BEZ", 1, 3), ("BEZ", 0, -2),
793
+ ]
794
+ m = RCPU(prog, k_regs=4, width=W, mem_words=4)
795
+ bad = 0
796
+ checked = 0
797
+ rng = __import__("random").Random(0)
798
+ for pc in range(len(prog)):
799
+ for br in (1, 2, 3, -2, -1):
800
+ for _ in range(60):
801
+ s0 = {"R": [rng.randint(0, (1 << W) - 1) for _ in range(4)],
802
+ "PC": pc, "BR": br,
803
+ "MEM": [rng.randint(0, (1 << W) - 1) for _ in range(4)]}
804
+ s = _clone(s0)
805
+ m.step(s)
806
+ m.step_back(s)
807
+ if not _eq(s, s0):
808
+ bad += 1
809
+ # and the other composition order
810
+ s = _clone(s0)
811
+ m.step_back(s)
812
+ m.step(s)
813
+ if not _eq(s, s0):
814
+ bad += 1
815
+ checked += 2
816
+ print(f" step_back o step = id over {checked} (state, instruction) cases: "
817
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
818
+ return bad == 0
819
+
820
+ WIDTH = 8
821
+
822
+ MASK = (1 << WIDTH) - 1
823
+
824
+
825
+ def _regs_written(stmts) -> set:
826
+ w = set()
827
+ for st in stmts:
828
+ op = st[0]
829
+ if op in ("ADD", "SUB", "ADDI", "XOR", "NEG"):
830
+ w.add(st[1])
831
+ elif op == "SWAP":
832
+ w.add(st[1]); w.add(st[2])
833
+ elif op == "FOR":
834
+ w |= _regs_written(st[2])
835
+ elif op == "IF":
836
+ w |= _regs_written(st[2]) | _regs_written(st[3])
837
+ return w
838
+
839
+
840
+ def invert(stmts: List[tuple]) -> List[tuple]:
841
+ out = []
842
+ for st in reversed(stmts):
843
+ op = st[0]
844
+ if op == "ADD":
845
+ out.append(("SUB", st[1], st[2]))
846
+ elif op == "SUB":
847
+ out.append(("ADD", st[1], st[2]))
848
+ elif op == "ADDI":
849
+ out.append(("ADDI", st[1], -st[2]))
850
+ elif op in ("XOR", "NEG", "SWAP"):
851
+ out.append(st)
852
+ elif op == "FOR":
853
+ out.append(("FOR", st[1], invert(st[2])))
854
+ elif op == "IF":
855
+ # Janus: reverse swaps predicate and exit assertion, inverts branches
856
+ out.append(("IF", st[4], invert(st[2]), invert(st[3]), st[1]))
857
+ return out
858
+
859
+
860
+ def prog_run(stmts: List[tuple], s: Dict[str, int]) -> Dict[str, int]:
861
+ for st in stmts:
862
+ op = st[0]
863
+ if op == "ADD":
864
+ s[st[1]] = (s[st[1]] + s[st[2]]) & MASK
865
+ elif op == "SUB":
866
+ s[st[1]] = (s[st[1]] - s[st[2]]) & MASK
867
+ elif op == "ADDI":
868
+ s[st[1]] = (s[st[1]] + st[2]) & MASK
869
+ elif op == "XOR":
870
+ s[st[1]] ^= s[st[2]]
871
+ elif op == "NEG":
872
+ s[st[1]] = (-s[st[1]]) & MASK
873
+ elif op == "SWAP":
874
+ s[st[1]], s[st[2]] = s[st[2]], s[st[1]]
875
+ elif op == "FOR":
876
+ cnt, body = st[1], st[2]
877
+ if cnt in _regs_written(body):
878
+ raise ValueError("FOR count register must be read-only (irreversible otherwise)")
879
+ for _ in range(s[cnt]):
880
+ prog_run(body, s)
881
+ elif op == "IF":
882
+ pred, then, els, exit_assert = st[1], st[2], st[3], st[4]
883
+ if pred(s):
884
+ prog_run(then, s)
885
+ assert exit_assert(s), "exit assertion violated (not reversible)"
886
+ else:
887
+ prog_run(els, s)
888
+ assert not exit_assert(s), "exit assertion violated (not reversible)"
889
+ return s
890
+
891
+
892
+ # --- demonstration programs ---
893
+ MULTIPLY = [("FOR", "b", [("ADD", "acc", "a")])] # acc += a, b times; a,b preserved
894
+
895
+ FIB = [("FOR", "n", [("ADD", "a", "b"), ("SWAP", "a", "b")])] # (a,b)->(b,a+b), n times
896
+
897
+
898
+ def _test():
899
+ ok = True
900
+
901
+ # reversible multiply: acc = a*b, inputs preserved; inverse clears acc
902
+ bad = 0
903
+ for a in range(16):
904
+ for b in range(16):
905
+ s = {"a": a, "b": b, "acc": 0}
906
+ prog_run(MULTIPLY, s)
907
+ if s["acc"] != (a * b) & MASK or s["a"] != a or s["b"] != b:
908
+ bad += 1
909
+ prog_run(invert(MULTIPLY), s) # run backward
910
+ if s != {"a": a, "b": b, "acc": 0}:
911
+ bad += 1
912
+ print(f" reversible multiply acc=a*b, inputs preserved, inverse clears acc: "
913
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
914
+ ok &= bad == 0
915
+
916
+ # reversible Fibonacci: n steps forward, inverse recovers the seed
917
+ bad = 0
918
+ for n in range(12):
919
+ s = {"a": 0, "b": 1, "n": n}
920
+ prog_run(FIB, s)
921
+ # forward value check against a plain reference
922
+ ra, rb = 0, 1
923
+ for _ in range(n):
924
+ ra, rb = rb, (ra + rb) & MASK
925
+ if (s["a"], s["b"]) != (ra, rb):
926
+ bad += 1
927
+ prog_run(invert(FIB), s)
928
+ if s != {"a": 0, "b": 1, "n": n}:
929
+ bad += 1
930
+ print(f" reversible Fibonacci n steps, inverse recovers seed: "
931
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
932
+ ok &= bad == 0
933
+
934
+ # a reversible conditional (Janus IF): swap when the operands differ. The
935
+ # exit assertion a != b is true exactly when the then-branch ran (swapping
936
+ # distinct values keeps them distinct; equal values are skipped), so the
937
+ # reverse picks the right branch.
938
+ prog = [("IF", lambda s: s["a"] != s["b"], [("SWAP", "a", "b")], [],
939
+ lambda s: s["a"] != s["b"])]
940
+ bad = 0
941
+ for a in range(16):
942
+ for b in range(16):
943
+ s = {"a": a, "b": b}
944
+ prog_run(prog, s)
945
+ if sorted([s["a"], s["b"]]) != sorted([a, b]):
946
+ bad += 1
947
+ prog_run(invert(prog), s)
948
+ if s != {"a": a, "b": b}:
949
+ bad += 1
950
+ print(f" reversible conditional (Janus IF with exit assertion): "
951
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
952
+ ok &= bad == 0
953
+ return ok
954
+
955
+
956
+ REVERSIBLE_OUT = os.path.join(ROOT, "variants", "neural_reversible.safetensors")
957
+
958
+ # The two reversible primitives, keyed to their gate functions.
959
+ _CODE = {CNOT: 0, TOFF: 1}
960
+
961
+ _FN = {0: CNOT, 1: TOFF}
962
+
963
+ # Heaviside threshold gates that implement the target updates: a CNOT target is
964
+ # XOR(t,c); a Toffoli target is XOR(t, AND(a,b)). AND/OR/NAND are single gates.
965
+ _SUBSTRATE = {
966
+ "and_w": torch.tensor([1, 1]), "and_b": torch.tensor(-2),
967
+ "or_w": torch.tensor([1, 1]), "or_b": torch.tensor(-1),
968
+ "nand_w": torch.tensor([-1, -1]), "nand_b": torch.tensor(1),
969
+ }
970
+
971
+
972
+ def encode(ops):
973
+ codes, args = [], []
974
+ for gate, *a in ops:
975
+ codes.append(_CODE[gate])
976
+ args.append((a + [-1, -1, -1])[:3])
977
+ return torch.tensor(codes, dtype=torch.long), torch.tensor(args, dtype=torch.long)
978
+
979
+
980
+ def main_build_reversible() -> int:
981
+ a_bits = list(range(WIDTH))
982
+ b_bits = list(range(WIDTH, 2 * WIDTH))
983
+ carry = 2 * WIDTH
984
+ n = 2 * WIDTH + 1
985
+ ops = _adder_ops(a_bits, b_bits, carry)
986
+ codes, args = encode(ops)
987
+
988
+ tensors = {"gate_code": codes, "gate_args": args, **_SUBSTRATE}
989
+ import json
990
+ meta = {"machine": "reversible", "width": str(WIDTH), "n_wires": str(n),
991
+ "a_bits": json.dumps(a_bits), "b_bits": json.dumps(b_bits),
992
+ "carry": str(carry), "circuit": "in-place reversible adder b<-a+b"}
993
+ save_file(tensors, REVERSIBLE_OUT, metadata=meta)
994
+ print(f"Built {os.path.relpath(REVERSIBLE_OUT, ROOT)}: reversible {WIDTH}-bit adder")
995
+ print(f" gates={len(ops)} wires={n} size={os.path.getsize(REVERSIBLE_OUT)} bytes")
996
+
997
+ # round-trip: reconstruct the op list from the file and run it
998
+ t = load_file(REVERSIBLE_OUT)
999
+ with safe_open(REVERSIBLE_OUT, framework="pt") as f:
1000
+ m = f.metadata()
1001
+ W = int(m["width"]); ab = json.loads(m["a_bits"]); bb = json.loads(m["b_bits"])
1002
+ cy = int(m["carry"]); nn = int(m["n_wires"])
1003
+ loaded = []
1004
+ for code, a in zip(t["gate_code"].tolist(), t["gate_args"].tolist()):
1005
+ loaded.append((_FN[code], *[x for x in a if x >= 0]))
1006
+
1007
+ def run(reg):
1008
+ for gate, *a in loaded:
1009
+ gate(reg, *a)
1010
+
1011
+ mask = (1 << W) - 1
1012
+ bad = 0
1013
+ rng = random.Random(0)
1014
+ for _ in range(400):
1015
+ a = rng.randint(0, mask); b = rng.randint(0, mask)
1016
+ reg = [0] * nn
1017
+ for k in range(W):
1018
+ reg[ab[k]] = (a >> k) & 1
1019
+ reg[bb[k]] = (b >> k) & 1
1020
+ run(reg)
1021
+ got_b = sum(reg[bb[k]] << k for k in range(W))
1022
+ got_a = sum(reg[ab[k]] << k for k in range(W))
1023
+ if got_b != ((a + b) & mask) or got_a != a or reg[cy] != 0:
1024
+ bad += 1
1025
+ print(f" round-trip b<-a+b, addend & carry restored (400 cases): "
1026
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
1027
+ # bijection is exhaustive-checkable at width 4 on the same construction
1028
+ a4 = list(range(4)); b4 = list(range(4, 8)); c4 = 8
1029
+ ops4 = _adder_ops(a4, b4, c4)
1030
+ perm = is_permutation(lambda r: _apply(r, ops4), 9)
1031
+ print(f" loaded circuit is a bijection (4-bit): {'OK' if perm else 'FAIL'}")
1032
+ return 0 if (bad == 0 and perm) else 1
1033
+
1034
+
1035
+ def adder_net(width: int):
1036
+ """The in-place Cuccaro adder as a feedforward ternary netlist computing its permutation."""
1037
+ a_bits = list(range(width))
1038
+ b_bits = list(range(width, 2 * width))
1039
+ carry = 2 * width
1040
+ n = 2 * width + 1
1041
+ ops = _adder_ops(a_bits, b_bits, carry)
1042
+ net = Net()
1043
+ inputs = [f"in{i}" for i in range(n)]
1044
+ cur = list(inputs)
1045
+ for k, (gate, *args) in enumerate(ops):
1046
+ if gate is CNOT:
1047
+ c, t = args
1048
+ cur[t] = net.XOR(f"c{k}", cur[t], cur[c]) # t ^= c
1049
+ else: # Toffoli: t ^= a&b
1050
+ a, b, t = args
1051
+ tmp = net.AND(f"t{k}a", [cur[a], cur[b]])
1052
+ cur[t] = net.XOR(f"t{k}x", cur[t], tmp)
1053
+ return net, inputs, list(cur), n, a_bits, b_bits, carry
1054
+
1055
+
1056
+ def _refs(n, a_bits, b_bits, carry):
1057
+ out = []
1058
+ for x in range(1 << n):
1059
+ reg = [(x >> i) & 1 for i in range(n)]
1060
+ _apply(reg, _adder_ops(a_bits, b_bits, carry))
1061
+ out.append(sum(reg[i] << i for i in range(n)))
1062
+ return torch.tensor(out)
1063
+
1064
+
1065
+ def _outputs(mm, states, n, **step_kw):
1066
+ v = mm.step(states.clone(), **step_kw)
1067
+ bits = (v >= 0.5).to(torch.int64)
1068
+ weights = torch.tensor([1 << i for i in range(n)])
1069
+ return (bits * weights).sum(dim=1)
1070
+
1071
+
1072
+ def analog_sweep(mm, states, refs, n):
1073
+ """The permutation must survive analog imperfection."""
1074
+ print(" read-noise sweep (bit-exact = all 512 outputs still match, 4 trials):")
1075
+ for sigma in (0.05, 0.10, 0.15, 0.20):
1076
+ ok = all((_outputs(mm, states, n, analog=True, noise_sigma=sigma,
1077
+ gen=torch.Generator().manual_seed(s)) == refs).all()
1078
+ for s in range(4))
1079
+ print(f" sigma={sigma:.2f}: {'bit-exact' if ok else 'errors appear'}")
1080
+ print(" conductance-mismatch sweep (fixed per-device weight perturbation):")
1081
+ for sg in (0.05, 0.10, 0.15):
1082
+ ok = (_outputs(mm.perturbed(sg, seed=0), states, n, analog=True) == refs).all()
1083
+ print(f" sigma_G={sg:.2f}: {'bit-exact' if ok else 'errors appear'}")
1084
+
1085
+
1086
+ def main_reversible_matrix() -> int:
1087
+ width = 4
1088
+ net, inputs, outputs, n, a_bits, b_bits, carry = adder_net(width)
1089
+ layers, info = compile_net(net, inputs, outputs)
1090
+ mm = MatrixMachine(layers)
1091
+
1092
+ states = torch.stack([torch.tensor([float((x >> i) & 1) for i in range(n)])
1093
+ for x in range(1 << n)])
1094
+ refs = _refs(n, a_bits, b_bits, carry)
1095
+ got = _outputs(mm, states, n)
1096
+ bad = int((got != refs).sum())
1097
+ perm = len(set(got.tolist())) == (1 << n)
1098
+ margin = mm.min_margin(states[:256])
1099
+
1100
+ print(f"reversible {width}-bit adder as a ternary matrix stack")
1101
+ print(f" layers={info['layers']} gates={info['gates']} "
1102
+ f"max_width={info['max_width']} total_weights={info['total_weights']}")
1103
+ print(f" every weight ternary: {'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}")
1104
+ print(f" matches the gate circuit over all {1 << n} inputs: {'OK' if bad == 0 else f'FAIL({bad})'}")
1105
+ print(f" composed transition is a permutation of the state space: {'OK' if perm else 'FAIL'}")
1106
+ print(f" analog noise margin, all layers: {margin:.3f} (guarantee 0.5)")
1107
+ analog_sweep(mm, states, refs, n)
1108
+ ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6
1109
+ print("PASS" if ok else "FAIL")
1110
+ return 0 if ok else 1
1111
+
1112
+ Block = Tuple[int, int, int, int]
1113
+
1114
+
1115
+ def rule(b: Block) -> Block:
1116
+ tl, tr, bl, br = b
1117
+ if b == (1, 0, 0, 1):
1118
+ return (0, 1, 1, 0) # diagonal pair -> other diagonal (deflect)
1119
+ if b == (0, 1, 1, 0):
1120
+ return (1, 0, 0, 1)
1121
+ return (br, bl, tr, tl) # otherwise rotate 180 degrees
1122
+
1123
+
1124
+ def is_bijection() -> bool:
1125
+ outs = {rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)}
1126
+ return len(outs) == 16
1127
+
1128
+
1129
+ def self_inverse() -> bool:
1130
+ return all(rule(rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))))
1131
+ == tuple((s >> k) & 1 for k in (3, 2, 1, 0)) for s in range(16))
1132
+
1133
+
1134
+ def step(grid: List[List[int]], phase: int) -> List[List[int]]:
1135
+ """One Margolus update; phase 0 aligns blocks at even coordinates, phase 1 offsets them by (1,1)."""
1136
+ H, W = len(grid), len(grid[0])
1137
+ out = [row[:] for row in grid]
1138
+ o = phase
1139
+ for r0 in range(o, o + H, 2):
1140
+ for c0 in range(o, o + W, 2):
1141
+ r, r1 = r0 % H, (r0 + 1) % H
1142
+ c, c1 = c0 % W, (c0 + 1) % W
1143
+ nb = rule((grid[r][c], grid[r][c1], grid[r1][c], grid[r1][c1]))
1144
+ out[r][c], out[r][c1], out[r1][c], out[r1][c1] = nb
1145
+ return out
1146
+
1147
+
1148
+ def ca_run(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
1149
+ g = grid
1150
+ for n in range(nsteps):
1151
+ g = step(g, (start_phase + n) & 1)
1152
+ return g
1153
+
1154
+
1155
+ def run_back(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
1156
+ """Undo `run`: replay the phase sequence in reverse; the rule is self-inverse."""
1157
+ phases = [(start_phase + n) & 1 for n in range(nsteps)]
1158
+ g = grid
1159
+ for p in reversed(phases):
1160
+ g = step(g, p)
1161
+ return g
1162
+
1163
+
1164
+ # --- the block rule as Heaviside threshold gates ---
1165
+ # rule(s) = rotate180(s) XOR is_diag(s) on every cell: rotation fixes diagonal
1166
+ # pairs, and flipping all four cells of a rotated diagonal pair sends it to the
1167
+ # other diagonal. is_diag detects the two diagonal-pair states.
1168
+ def _H(x):
1169
+ return 1 if x >= 0 else 0
1170
+
1171
+
1172
+ def _and(*xs):
1173
+ return _H(sum(xs) - len(xs))
1174
+
1175
+
1176
+ def _or(*xs):
1177
+ return _H(sum(xs) - 1)
1178
+
1179
+
1180
+ def _xor(a, b):
1181
+ return _and(_or(a, b), _H(1 - a - b)) # OR AND NAND, the family's XOR
1182
+
1183
+
1184
+ def gate_rule(b: Block) -> Block:
1185
+ tl, tr, bl, br = b
1186
+ d = _or(_and(tl, 1 - tr, 1 - bl, br), _and(1 - tl, tr, bl, 1 - br)) # is_diag
1187
+ return (_xor(br, d), _xor(bl, d), _xor(tr, d), _xor(tl, d))
1188
+
1189
+
1190
+ def _test_gates():
1191
+ ok = all(gate_rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0)))
1192
+ == rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16))
1193
+ print(f" block rule as Heaviside threshold gates matches over 16 states: "
1194
+ f"{'OK' if ok else 'FAIL'}")
1195
+ return ok
1196
+
1197
+
1198
+ # --- tests ---
1199
+ def _rand_grid(H, W, seed):
1200
+ import random
1201
+ rng = random.Random(seed)
1202
+ return [[rng.randint(0, 1) for _ in range(W)] for _ in range(H)]
1203
+
1204
+
1205
+ def _ball_positions(g):
1206
+ return {(r, c) for r, row in enumerate(g) for c, v in enumerate(row) if v}
1207
+
1208
+
1209
+ def _test_rule():
1210
+ print(f" block rule is a bijection of 16 states: {'OK' if is_bijection() else 'FAIL'}")
1211
+ print(f" block rule is self-inverse: {'OK' if self_inverse() else 'FAIL'}")
1212
+ return is_bijection() and self_inverse()
1213
+
1214
+
1215
+ def _test_reversibility():
1216
+ bad = 0
1217
+ for seed in range(20):
1218
+ g = _rand_grid(8, 8, seed)
1219
+ fwd = ca_run(g, 25, start_phase=0)
1220
+ back = run_back(fwd, 25, start_phase=0)
1221
+ if back != g:
1222
+ bad += 1
1223
+ # particle count is conserved (the rule permutes cells within each block)
1224
+ g = _rand_grid(8, 8, 99)
1225
+ conserved = sum(sum(r) for r in g) == sum(sum(r) for r in ca_run(g, 40))
1226
+ print(f" lattice reversible (run then reverse recovers grid, 20 grids): "
1227
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
1228
+ print(f" particle number conserved: {'OK' if conserved else 'FAIL'}")
1229
+ return bad == 0 and conserved
1230
+
1231
+
1232
+ def _test_ballistic():
1233
+ # a single particle travels in a straight diagonal line
1234
+ H = W = 16
1235
+ g = [[0] * W for _ in range(H)]
1236
+ g[2][2] = 1
1237
+ positions = [next(iter(_ball_positions(g)))]
1238
+ gg = g
1239
+ for n in range(8):
1240
+ gg = step(gg, n & 1)
1241
+ p = _ball_positions(gg)
1242
+ positions.append(next(iter(p)) if len(p) == 1 else None)
1243
+ ok = all(p is not None for p in positions)
1244
+ steady = ok and all(positions[i + 1] == (positions[i][0] + 1, positions[i][1] + 1)
1245
+ for i in range(len(positions) - 1))
1246
+ print(f" single particle stays a single particle: {'OK' if ok else 'FAIL'}")
1247
+ print(f" and moves ballistically on the diagonal, +(1,1) per step: "
1248
+ f"{'OK' if steady else 'FAIL'} trace={positions[:5]}")
1249
+ return ok and steady
1250
+
1251
+
1252
+ def interaction_gate(a: int, b: int) -> dict:
1253
+ """One billiard-ball collision as a reversible interaction gate."""
1254
+ H = W = 12
1255
+ g = [[0] * W for _ in range(H)]
1256
+ if a:
1257
+ g[2][2] = 1
1258
+ if b:
1259
+ g[7][7] = 1
1260
+ g = ca_run(g, 4)
1261
+ return {"A_and_B": g[3][6], "A_and_notB": g[6][6], "notA_and_B": g[3][3]}
1262
+
1263
+
1264
+ def and_gate(a: int, b: int) -> int:
1265
+ return interaction_gate(a, b)["A_and_B"]
1266
+
1267
+
1268
+ def _test_gate():
1269
+ ok = True
1270
+ for a in (0, 1):
1271
+ for b in (0, 1):
1272
+ o = interaction_gate(a, b)
1273
+ ok &= (o["A_and_B"] == (a & b) and o["A_and_notB"] == (a & (1 - b))
1274
+ and o["notA_and_B"] == ((1 - a) & b))
1275
+ print(f" billiard-ball interaction gate (A&B, A&~B, ~A&B) over all 4 inputs: "
1276
+ f"{'OK' if ok else 'FAIL'}")
1277
+ return ok
1278
+
1279
+
1280
+ def and3(a: int, b: int, c: int) -> int:
1281
+ """Two composed collisions."""
1282
+ H = W = 16
1283
+ g = [[0] * W for _ in range(H)]
1284
+ if a:
1285
+ g[2][2] = 1
1286
+ if b:
1287
+ g[7][7] = 1
1288
+ if c:
1289
+ g[0][9] = 1
1290
+ return ca_run(g, 4)[3][5]
1291
+
1292
+
1293
+ def _test_compose():
1294
+ ok = all(and3(a, b, c) == (a & b & c)
1295
+ for a in (0, 1) for b in (0, 1) for c in (0, 1))
1296
+ print(f" composed 3-input AND (two chained collisions) over all 8 inputs: "
1297
+ f"{'OK' if ok else 'FAIL'}")
1298
+ return ok
1299
+
1300
+
1301
+ def _test_collision():
1302
+ # Two particles interact (the joint evolution differs from independent
1303
+ # motion) and the collision stays reversible: the physics that logic needs.
1304
+ H = W = 12
1305
+ interacted = False
1306
+ revok = True
1307
+ # converging pairs: an SE-mover (even,even) meets an NW-mover (odd,odd) on a
1308
+ # shared diagonal, forming the 1001/0110 diagonal pair the rule deflects.
1309
+ for a, b in [((2, 2), (7, 7)), ((3, 3), (8, 8)), ((2, 8), (7, 3)),
1310
+ ((4, 4), (9, 9)), ((2, 2), (9, 9))]:
1311
+ g = [[0] * W for _ in range(H)]
1312
+ g[a[0]][a[1]] = 1
1313
+ g[b[0]][b[1]] = 1
1314
+ ga = [[0] * W for _ in range(H)]
1315
+ ga[a[0]][a[1]] = 1
1316
+ gb = [[0] * W for _ in range(H)]
1317
+ gb[b[0]][b[1]] = 1
1318
+ joint = g
1319
+ for n in range(12):
1320
+ joint = step(joint, n & 1)
1321
+ ga = step(ga, n & 1)
1322
+ gb = step(gb, n & 1)
1323
+ free = _ball_positions(ga) | _ball_positions(gb)
1324
+ if _ball_positions(joint) != free:
1325
+ interacted = True
1326
+ if run_back(ca_run(g, 12), 12) != g:
1327
+ revok = False
1328
+ print(f" two-particle collisions interact (joint != independent motion): "
1329
+ f"{'OK' if interacted else 'FAIL'}")
1330
+ print(f" collisions remain reversible: {'OK' if revok else 'FAIL'}")
1331
+ return interacted and revok
1332
+
1333
+ CA_OUT = os.path.join(ROOT, "variants", "neural_ca.safetensors")
1334
+
1335
+
1336
+ def block_net():
1337
+ net = Net()
1338
+ ntl, ntr = net.NOT("ntl", "tl"), net.NOT("ntr", "tr")
1339
+ nbl, nbr = net.NOT("nbl", "bl"), net.NOT("nbr", "br")
1340
+ d1 = net.AND("d1", ["tl", ntr, nbl, "br"])
1341
+ d2 = net.AND("d2", [ntl, "tr", "bl", nbr])
1342
+ dg = net.OR("dg", [d1, d2]) # is_diag
1343
+ TL = net.XOR("oTL", "br", dg)
1344
+ TR = net.XOR("oTR", "bl", dg)
1345
+ BL = net.XOR("oBL", "tr", dg)
1346
+ BR = net.XOR("oBR", "tl", dg)
1347
+ return net, ["tl", "tr", "bl", "br"], [TL, TR, BL, BR]
1348
+
1349
+
1350
+ def matrix_step(mm, grid, phase):
1351
+ """One Margolus update driven entirely by the matrix tile."""
1352
+ H, W = len(grid), len(grid[0])
1353
+ out = [row[:] for row in grid]
1354
+ for r0 in range(phase, phase + H, 2):
1355
+ for c0 in range(phase, phase + W, 2):
1356
+ r, r1, c, c1 = r0 % H, (r0 + 1) % H, c0 % W, (c0 + 1) % W
1357
+ v = torch.tensor([[float(grid[r][c]), float(grid[r][c1]),
1358
+ float(grid[r1][c]), float(grid[r1][c1])]])
1359
+ o = mm.step(v)[0]
1360
+ out[r][c], out[r][c1], out[r1][c], out[r1][c1] = (int(o[0]), int(o[1]),
1361
+ int(o[2]), int(o[3]))
1362
+ return out
1363
+
1364
+
1365
+ def main_build_ca() -> int:
1366
+ net, inp, outp = block_net()
1367
+ layers, info = compile_net(net, inp, outp)
1368
+ mm = MatrixMachine(layers)
1369
+
1370
+ seen, bad, vecs = set(), 0, []
1371
+ for s in range(16):
1372
+ b = tuple((s >> k) & 1 for k in (3, 2, 1, 0)) # tl,tr,bl,br
1373
+ v = torch.tensor([[float(x) for x in b]])
1374
+ got = tuple(int(x) for x in mm.step(v)[0])
1375
+ if got != rule(b):
1376
+ bad += 1
1377
+ seen.add(got)
1378
+ vecs.append(v[0])
1379
+ perm = len(seen) == 16
1380
+ margin = mm.min_margin(torch.stack(vecs))
1381
+
1382
+ tensors = {}
1383
+ for k, (W, B) in enumerate(layers):
1384
+ tensors[f"matrix.layer{k:03d}.weight"] = W.to(torch.int8)
1385
+ tensors[f"matrix.layer{k:03d}.bias"] = B.to(torch.int8)
1386
+ meta = {"machine": "ca",
1387
+ "rule": "Margolus reversible: rotate 180 except diagonal pair swaps (BBM class)",
1388
+ "inputs": "tl,tr,bl,br", "outputs": "TL,TR,BL,BR", "layers": str(info["layers"])}
1389
+ save_file(tensors, CA_OUT, metadata=meta)
1390
+ print(f"Built {os.path.relpath(CA_OUT, ROOT)}: reversible CA block rule as a ternary matrix tile")
1391
+ print(f" layers={info['layers']} gates={info['gates']} size={os.path.getsize(CA_OUT)} bytes")
1392
+ print(f" every weight ternary: "
1393
+ f"{'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}")
1394
+ print(f" tile matches the block rule over all 16 states: {'OK' if bad == 0 else f'FAIL({bad})'}")
1395
+ print(f" tile is a permutation (16 distinct outputs): {'OK' if perm else 'FAIL'}")
1396
+ print(f" analog noise margin: {margin:.3f} (guarantee 0.5)")
1397
+
1398
+ # analog robustness: the tile must reproduce the rule under read noise and
1399
+ # static conductance mismatch, as neural_matrix8 measures
1400
+ states = torch.stack(vecs)
1401
+ ref = [rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)]
1402
+
1403
+ def outs(machine, **kw):
1404
+ v = machine.step(states, **kw)
1405
+ return [tuple(int(v[i][j]) for j in range(4)) for i in range(v.shape[0])]
1406
+
1407
+ print(" read-noise sweep (exact = tile matches the rule on all 16 states):")
1408
+ for sigma in (0.05, 0.10, 0.15, 0.20):
1409
+ okn = all(outs(mm, analog=True, noise_sigma=sigma,
1410
+ gen=torch.Generator().manual_seed(s)) == ref for s in range(4))
1411
+ print(f" sigma={sigma:.2f}: {'exact' if okn else 'errors appear'}")
1412
+ print(" conductance-mismatch sweep:")
1413
+ for sg in (0.05, 0.10, 0.15):
1414
+ okg = outs(mm.perturbed(sg, seed=0), analog=True) == ref
1415
+ print(f" sigma_G={sg:.2f}: {'exact' if okg else 'errors appear'}")
1416
+
1417
+ # the loaded tile, applied to every block, is one whole-lattice CA step
1418
+ t = load_file(CA_OUT)
1419
+ n = 0
1420
+ lyr = []
1421
+ while f"matrix.layer{n:03d}.weight" in t:
1422
+ lyr.append((t[f"matrix.layer{n:03d}.weight"], t[f"matrix.layer{n:03d}.bias"]))
1423
+ n += 1
1424
+ mm2 = MatrixMachine(lyr)
1425
+ g = _rand_grid(8, 8, 3)
1426
+ gmat = matrix_step(mm2, g, 0)
1427
+ gref = step(g, 0)
1428
+ print(f" loaded tile drives a full lattice step (matches ca.step): "
1429
+ f"{'OK' if gmat == gref else 'FAIL'}")
1430
+
1431
+ ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6 and gmat == gref
1432
+ print("PASS" if ok else "FAIL")
1433
+ return 0 if ok else 1
1434
+
1435
+ # side -> (dx, dy, my_side, neighbor_side)
1436
+ _SIDES = [(0, 1, "N", "S"), (1, 0, "E", "W"), (0, -1, "S", "N"), (-1, 0, "W", "E")]
1437
+
1438
+
1439
+ class Tile:
1440
+ __slots__ = ("N", "E", "S", "W", "name")
1441
+
1442
+ def __init__(self, N="", E="", S="", W="", name=""):
1443
+ self.N, self.E, self.S, self.W, self.name = N, E, S, W, name
1444
+
1445
+ def glue(self, side):
1446
+ return getattr(self, side)
1447
+
1448
+
1449
+ def bind_strength(A: Dict[Tuple[int, int], Tile], x: int, y: int, t: Tile,
1450
+ strength: Dict[str, int]) -> int:
1451
+ """Summed strength of t's glues that match the abutting neighbor glues."""
1452
+ s = 0
1453
+ for dx, dy, side, opp in _SIDES:
1454
+ nb = A.get((x + dx, y + dy))
1455
+ if nb is None:
1456
+ continue
1457
+ g = t.glue(side)
1458
+ if g and g == nb.glue(opp):
1459
+ s += strength.get(g, 1)
1460
+ return s
1461
+
1462
+
1463
+ def binds(A, x, y, t, tau, strength) -> bool:
1464
+ """The threshold-gate binding decision: H(sum strength*match - tau)."""
1465
+ return bind_strength(A, x, y, t, strength) >= tau
1466
+
1467
+
1468
+ def grow(tileset: List[Tile], seed: Dict[Tuple[int, int], Tile], tau: int,
1469
+ strength: Dict[str, int], bounds: Tuple[int, int, int, int],
1470
+ max_tiles: int = 100000) -> Tuple[Dict[Tuple[int, int], Tile], bool]:
1471
+ """Directed growth from a seed."""
1472
+ x0, y0, x1, y1 = bounds
1473
+ A = dict(seed)
1474
+ deterministic = True
1475
+ changed = True
1476
+ while changed and len(A) < max_tiles:
1477
+ changed = False
1478
+ frontier = set()
1479
+ for (x, y) in list(A):
1480
+ for dx, dy, _, _ in _SIDES:
1481
+ p = (x + dx, y + dy)
1482
+ if p not in A and x0 <= p[0] <= x1 and y0 <= p[1] <= y1:
1483
+ frontier.add(p)
1484
+ for (x, y) in frontier:
1485
+ binders = [t for t in tileset if binds(A, x, y, t, tau, strength)]
1486
+ if len(binders) == 1:
1487
+ A[(x, y)] = binders[0]
1488
+ changed = True
1489
+ elif len(binders) > 1:
1490
+ deterministic = False
1491
+ return A, deterministic
1492
+
1493
+
1494
+ # ---------------------------------------------------------------------------
1495
+ # XOR / Sierpinski tile set: value(x,y) = value(x-1,y) XOR value(x,y-1)
1496
+ # ---------------------------------------------------------------------------
1497
+ def rule2_tileset(fn) -> List[Tile]:
1498
+ """Rule tiles for value(x,y) = fn(W, S): four cooperatively binding tiles emitting fn north and east."""
1499
+ ts = []
1500
+ for s in (0, 1):
1501
+ for w in (0, 1):
1502
+ v = fn(w, s)
1503
+ ts.append(Tile(N=f"v{v}", E=f"v{v}", S=f"v{s}", W=f"v{w}",
1504
+ name=f"R w{w} s{s} -> {v}"))
1505
+ return ts
1506
+
1507
+
1508
+ def sierpinski_tileset() -> List[Tile]:
1509
+ return rule2_tileset(lambda w, s: w ^ s)
1510
+
1511
+
1512
+ def _row_col_seed(bottom: List[int], left: List[int]):
1513
+ """Seed the bottom row and the left column with fixed value tiles."""
1514
+ seed = {}
1515
+ for x, b in enumerate(bottom):
1516
+ seed[(x, 0)] = Tile(N=f"v{b}", E="", S="", W="", name=f"seedB{x}={b}")
1517
+ for y, l in enumerate(left):
1518
+ if y == 0:
1519
+ continue
1520
+ seed[(0, y)] = Tile(N="", E=f"v{l}", S="", W="", name=f"seedL{y}={l}")
1521
+ return seed
1522
+
1523
+
1524
+ def _test_binding_gate():
1525
+ """The binding decision is exactly the Heaviside threshold gate."""
1526
+ strength = {"v0": 1, "v1": 1}
1527
+ ts = sierpinski_tileset()
1528
+ A = {(1, 0): Tile(N="v1"), (0, 1): Tile(E="v0")}
1529
+ bad = 0
1530
+ for t in ts:
1531
+ for x, y in [(1, 1)]:
1532
+ w = sum(strength.get(t.glue(side), 1)
1533
+ for dx, dy, side, opp in _SIDES
1534
+ if A.get((x + dx, y + dy)) and t.glue(side)
1535
+ and t.glue(side) == A[(x + dx, y + dy)].glue(opp))
1536
+ gate = 1 if (w - 2) >= 0 else 0 # H(sum*match - tau)
1537
+ if gate != int(binds(A, x, y, t, 2, strength)):
1538
+ bad += 1
1539
+ print(f" binding decision == Heaviside gate H(sum-tau): {'OK' if bad == 0 else 'FAIL'}")
1540
+ return bad == 0
1541
+
1542
+
1543
+ def _test_rule2(fn, name, n=24):
1544
+ strength = {"v0": 1, "v1": 1}
1545
+ bottom = [1 if x == 0 else 0 for x in range(n)]
1546
+ left = [1 if y == 0 else 0 for y in range(n)]
1547
+ seed = _row_col_seed(bottom, left)
1548
+ A, det = grow(rule2_tileset(fn), seed, 2, strength, (0, 0, n - 1, n - 1))
1549
+
1550
+ def val(x, y):
1551
+ t = A.get((x, y))
1552
+ return None if t is None else (1 if t.N == "v1" else 0)
1553
+
1554
+ ref = {(x, 0): bottom[x] for x in range(n)}
1555
+ ref.update({(0, y): left[y] for y in range(n)})
1556
+ for y in range(1, n):
1557
+ for x in range(1, n):
1558
+ ref[(x, y)] = fn(ref[(x - 1, y)], ref[(x, y - 1)])
1559
+
1560
+ filled = bad = 0
1561
+ for y in range(1, n):
1562
+ for x in range(1, n):
1563
+ v = val(x, y)
1564
+ if v is not None:
1565
+ filled += 1
1566
+ bad += v != ref[(x, y)]
1567
+ tag = "OK" if (det and bad == 0 and filled > 0) else "FAIL"
1568
+ print(f" rule-tile CA fn={name:3s}: directed={det} placed={filled} "
1569
+ f"every tile = fn(W,S) {tag}")
1570
+ return det and bad == 0 and filled > 0
1571
+
1572
+
1573
+ # ---------------------------------------------------------------------------
1574
+ # Binary counter: each row is the row below plus one. LSB is the right column;
1575
+ # carry propagates west by cooperative binding (S = bit below, E = carry in).
1576
+ # ---------------------------------------------------------------------------
1577
+ def counter_tileset() -> List[Tile]:
1578
+ ts = []
1579
+ for b in (0, 1):
1580
+ for c in (0, 1):
1581
+ ts.append(Tile(N=f"b{b ^ c}", E=f"c{c}", S=f"b{b}", W=f"c{b & c}",
1582
+ name=f"C b{b} c{c} -> b{b ^ c} carry{b & c}"))
1583
+ ts.append(Tile(N="edge", E="", S="edge", W="c1", name="edge(+1 injector)"))
1584
+ return ts
1585
+
1586
+
1587
+ def counter_seed(n: int):
1588
+ """Bottom row (y=0) all zero, plus the right-edge +1 injector column base."""
1589
+ seed = {}
1590
+ for x in range(n):
1591
+ seed[(x, 0)] = Tile(N="b0", name=f"seed b0 col{x}")
1592
+ seed[(n, 0)] = Tile(N="edge", W="c1", name="seed edge")
1593
+ return seed
1594
+
1595
+
1596
+ def _test_counter(n=6, rows=None):
1597
+ rows = rows or (1 << n) - 1
1598
+ strength = {"edge": 2} # value/carry glues default 1
1599
+ A, det = grow(counter_tileset(), counter_seed(n), 2, strength,
1600
+ (0, 0, n, rows))
1601
+
1602
+ def rowval(y):
1603
+ bits = []
1604
+ for x in range(n):
1605
+ t = A.get((x, y))
1606
+ if t is None:
1607
+ return None
1608
+ bits.append(1 if t.N == "b1" else 0)
1609
+ return sum(bit << (n - 1 - x) for x, bit in enumerate(bits))
1610
+
1611
+ bad = filled = 0
1612
+ for y in range(1, rows + 1):
1613
+ v = rowval(y)
1614
+ if v is not None:
1615
+ filled += 1
1616
+ if v != (y & ((1 << n) - 1)):
1617
+ bad += 1
1618
+ print(f" binary counter {n}-bit: directed={det} rows grown={filled} "
1619
+ f"row y encodes the integer y {'OK' if bad == 0 else f'FAIL({bad})'}")
1620
+ return det and bad == 0 and filled == rows
1621
+
1622
+
1623
+ TILE_OUT = os.path.join(ROOT, "variants", "neural_tile.safetensors")
1624
+
1625
+ NBITS = 8
1626
+
1627
+ TAU = 2
1628
+
1629
+
1630
+ def main_build_tile() -> int:
1631
+ ts = counter_tileset()
1632
+ strength = {"edge": 2}
1633
+ glues = sorted({g for t in ts for g in (t.N, t.E, t.S, t.W) if g})
1634
+ gid = {g: i for i, g in enumerate(glues)}
1635
+ tile_glues = torch.tensor([[gid.get(t.N, -1), gid.get(t.E, -1),
1636
+ gid.get(t.S, -1), gid.get(t.W, -1)] for t in ts],
1637
+ dtype=torch.long)
1638
+ glue_strength = torch.tensor([strength.get(g, 1) for g in glues], dtype=torch.long)
1639
+ # per-tile binding-gate weights = strengths of the tile's four glues (0 = null)
1640
+ bind_w = torch.tensor([[strength.get(g, 1) if g else 0 for g in (t.N, t.E, t.S, t.W)]
1641
+ for t in ts], dtype=torch.long)
1642
+ tensors = {"tile_glues": tile_glues, "glue_strength": glue_strength,
1643
+ "binding_weight": bind_w, "binding_bias": torch.tensor(-TAU)}
1644
+ meta = {"machine": "tile", "tau": str(TAU), "glues": json.dumps(glues),
1645
+ "tile_names": json.dumps([t.name for t in ts]), "program": "binary counter"}
1646
+ save_file(tensors, TILE_OUT, metadata=meta)
1647
+ print(f"Built {os.path.relpath(TILE_OUT, ROOT)}: binary-counter tile set")
1648
+ print(f" tiles={len(ts)} glues={len(glues)} tau={TAU} size={os.path.getsize(TILE_OUT)} bytes")
1649
+
1650
+ # round-trip: reconstruct the tiles from the file and regrow the counter
1651
+ t = load_file(TILE_OUT)
1652
+ with safe_open(TILE_OUT, framework="pt") as f:
1653
+ m = f.metadata()
1654
+ gl = json.loads(m["glues"])
1655
+ strg = {gl[i]: int(s) for i, s in enumerate(t["glue_strength"].tolist())}
1656
+ tiles = []
1657
+ for row, name in zip(t["tile_glues"].tolist(), json.loads(m["tile_names"])):
1658
+ sides = [gl[i] if i >= 0 else "" for i in row]
1659
+ tiles.append(Tile(N=sides[0], E=sides[1], S=sides[2], W=sides[3], name=name))
1660
+
1661
+ rows = (1 << NBITS) - 1
1662
+ A, det = grow(tiles, counter_seed(NBITS), int(m["tau"]), strg,
1663
+ (0, 0, NBITS, rows))
1664
+ bad = filled = 0
1665
+ for y in range(1, rows + 1):
1666
+ cells = [A.get((x, y)) for x in range(NBITS)]
1667
+ if any(c is None for c in cells):
1668
+ continue
1669
+ filled += 1
1670
+ v = sum((1 if c.N == "b1" else 0) << (NBITS - 1 - x) for x, c in enumerate(cells))
1671
+ if v != (y & ((1 << NBITS) - 1)):
1672
+ bad += 1
1673
+ print(f" round-trip regrow {NBITS}-bit counter: directed={det} rows={filled} "
1674
+ f"row y == y {'OK' if bad == 0 else f'FAIL({bad})'}")
1675
+
1676
+ # the stored binding gate reproduces the model's binding decision
1677
+ gate_ok = True
1678
+ Atest = {(1, 0): Tile(N="b0"), (2, 0): Tile(N="b0")}
1679
+ for ti, tt in enumerate(tiles):
1680
+ for site in [(1, 1), (2, 1)]:
1681
+ w = t["binding_weight"][ti].tolist()
1682
+ match = [1 if tt.glue(side) and Atest.get((site[0] + dx, site[1] + dy))
1683
+ and tt.glue(side) == Atest[(site[0] + dx, site[1] + dy)].glue(opp) else 0
1684
+ for dx, dy, side, opp in _SIDES]
1685
+ gate = 1 if sum(wi * mi for wi, mi in zip(w, match)) + int(t["binding_bias"]) >= 0 else 0
1686
+ if gate != int(binds(Atest, site[0], site[1], tt, TAU, strg)):
1687
+ gate_ok = False
1688
+ print(f" stored binding gate H(weight.match - tau) matches growth rule: "
1689
+ f"{'OK' if gate_ok else 'FAIL'}")
1690
+ return 0 if (bad == 0 and det and gate_ok) else 1
1691
+
1692
+
1693
+ def cmd_attractor():
1694
+ print("Attractor computer\n" + "=" * 40)
1695
+ print("Forward evaluation (exact, energy 0):")
1696
+ a = test_forward()
1697
+ print("Canonical relaxation:")
1698
+ b = test_energy_relax()
1699
+ print("Backward inversion (factoring by relaxation):")
1700
+ c_ = test_factor()
1701
+ print("SAT (clamp output to 1, relax to a model):")
1702
+ d = test_sat()
1703
+ print("=" * 40)
1704
+ print("ALL PASS" if (a and b and c_ and d) else "FAILURES")
1705
+ sys.exit(0 if (a and b and c_ and d) else 1)
1706
+
1707
+
1708
+ def cmd_reversible():
1709
+ print("Reversible primitives + ALU")
1710
+ a = _test_primitives()
1711
+ b = _test_adder(4) and _test_adder(5)
1712
+ c = _test_alu(4)
1713
+ d = _test_bennett(4)
1714
+ print("PASS" if (a and b and c and d) else "FAIL")
1715
+
1716
+
1717
+ def cmd_reversible_cpu():
1718
+ print("Reversible CPU")
1719
+ a = test_straight_line()
1720
+ b = test_bijection()
1721
+ print("PASS" if (a and b) else "FAIL")
1722
+
1723
+
1724
+ def cmd_reversible_prog():
1725
+ print("Reversible structured programs")
1726
+ print("PASS" if _test() else "FAIL")
1727
+
1728
+
1729
+ def cmd_ca():
1730
+ print("Reversible Margolus cellular automaton")
1731
+ a = _test_rule()
1732
+ g = _test_gates()
1733
+ b = _test_reversibility()
1734
+ c = _test_ballistic()
1735
+ d = _test_collision()
1736
+ e = _test_gate()
1737
+ f = _test_compose()
1738
+ print("PASS" if (a and g and b and c and d and e and f) else "FAIL")
1739
+
1740
+
1741
+ def cmd_tile():
1742
+ print("Self-assembling tile computer")
1743
+ a = _test_binding_gate()
1744
+ b = all(_test_rule2(fn, nm) for fn, nm in
1745
+ [(lambda w, s: w ^ s, "XOR"), (lambda w, s: w & s, "AND"),
1746
+ (lambda w, s: w | s, "OR")])
1747
+ c = _test_counter()
1748
+ print("PASS" if (a and b and c) else "FAIL")
1749
+
1750
+
1751
+ COMMANDS = {'attractor': cmd_attractor, 'build-attractor': main_build_attractor, 'reversible': cmd_reversible, 'reversible-cpu': cmd_reversible_cpu, 'reversible-prog': cmd_reversible_prog, 'build-reversible': main_build_reversible, 'reversible-matrix': main_reversible_matrix, 'ca': cmd_ca, 'build-ca': main_build_ca, 'tile': cmd_tile, 'build-tile': main_build_tile}
1752
+
1753
+
1754
+ if __name__ == "__main__":
1755
+ if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
1756
+ sys.exit('commands: ' + ', '.join(COMMANDS))
1757
+ cmd = sys.argv.pop(1)
1758
+ sys.argv[0] += ' ' + cmd
1759
+ sys.exit(COMMANDS[cmd]())
src/build.py CHANGED
@@ -1,111 +1,16 @@
1
- """
2
- Build tools for 8-bit Threshold Computer safetensors.
3
-
4
- Subcommands:
5
- python build.py memory - Generate 64KB memory circuits
6
- python build.py inputs - Add .inputs metadata tensors
7
- python build.py all - Run both (memory first, then inputs)
8
-
9
-
10
- ROUTING SCHEMA (formerly routing.json)
11
- ======================================
12
-
13
- Routing info is now embedded in safetensors via .inputs tensors and signal registry metadata.
14
-
15
-
16
- INPUT SOURCE TYPES
17
- ------------------
18
-
19
- 1. External input: "$input_name" - Named input to the circuit
20
- - Example: "$a", "$b", "$cin"
21
-
22
- 2. Gate output: "path.to.gate" - Output of another gate
23
- - Example: "ha1.sum", "layer1.or"
24
-
25
- 3. Bit extraction: "$input[i]" - Single bit from multi-bit input
26
- - Example: "$a[0]" (LSB), "$a[7]" (MSB for 8-bit)
27
-
28
- 4. Constant: "#0" or "#1" - Fixed value
29
- - Example: "#1" for carry-in in two's complement
30
-
31
-
32
- CIRCUIT TYPES
33
- -------------
34
-
35
- Single-Layer Gates: .weight and .bias only
36
- "boolean.and": ["$a", "$b"]
37
-
38
- Two-Layer Gates (XOR, XNOR): layer1 + layer2
39
- "boolean.xor.layer1.or": ["$a", "$b"]
40
- "boolean.xor.layer1.nand": ["$a", "$b"]
41
- "boolean.xor.layer2": ["layer1.or", "layer1.nand"]
42
-
43
- Hierarchical Circuits: nested sub-components
44
- "arithmetic.fulladder": {
45
- "ha1.sum.layer1.or": ["$a", "$b"],
46
- "ha1.carry": ["$a", "$b"],
47
- "ha2.sum.layer1.or": ["ha1.sum", "$cin"],
48
- "carry_or": ["ha1.carry", "ha2.carry"]
49
- }
50
-
51
- Bit-Indexed Circuits: multi-bit operations
52
- "arithmetic.ripplecarry8bit.fa0": ["$a[0]", "$b[0]", "#0"]
53
- "arithmetic.ripplecarry8bit.fa1": ["$a[1]", "$b[1]", "fa0.cout"]
54
-
55
-
56
- PACKED MEMORY CIRCUITS
57
- ----------------------
58
-
59
- 64KB memory uses packed tensors (shapes for 16-bit address, 8-bit data):
60
-
61
- memory.addr_decode.weight: [65536, 16]
62
- memory.addr_decode.bias: [65536]
63
- memory.read.and.weight: [8, 65536, 2]
64
- memory.read.and.bias: [8, 65536]
65
- memory.read.or.weight: [8, 65536]
66
- memory.read.or.bias: [8]
67
- memory.write.sel.weight: [65536, 2]
68
- memory.write.sel.bias: [65536]
69
- memory.write.nsel.weight: [65536, 1]
70
- memory.write.nsel.bias: [65536]
71
- memory.write.and_old.weight: [65536, 8, 2]
72
- memory.write.and_old.bias: [65536, 8]
73
- memory.write.and_new.weight: [65536, 8, 2]
74
- memory.write.and_new.bias: [65536, 8]
75
- memory.write.or.weight: [65536, 8, 2]
76
- memory.write.or.bias: [65536, 8]
77
-
78
- Semantics:
79
- decode: sel[i] = H(sum(addr_bits * weight[i]) + bias[i])
80
- read: bit[b] = H(sum(H([mem_bit, sel] * and_w) + and_b) * or_w + or_b)
81
- write: new = H(H([old, nsel] * and_old) + H([data, sel] * and_new) - 1)
82
-
83
-
84
- SIGNAL REGISTRY
85
- ---------------
86
-
87
- Signal IDs are stored in safetensors metadata as JSON:
88
-
89
- {"0": "#0", "1": "#1", "2": "$a", "3": "$b", ...}
90
-
91
- Each gate's .inputs tensor contains integer IDs referencing this registry.
92
-
93
-
94
- NAMING CONVENTIONS
95
- ------------------
96
-
97
- - External inputs: $name or $name[bit]
98
- - Constants: #0, #1
99
- - Internal gates: relative path from circuit root
100
- """
101
-
102
  from __future__ import annotations
103
 
104
  import argparse
105
  import json
 
106
  import re
 
 
 
 
107
  from pathlib import Path
108
- from typing import Any, Dict, Iterable, List, Optional, Set
109
 
110
  import torch
111
  from safetensors import safe_open
@@ -140,9 +45,11 @@ def get_model_path(bits: int = 8, memory_profile: str = None, addr_bits: int = N
140
 
141
 
142
  MODEL_PATH = MODEL_DIR / "neural_computer8.safetensors"
 
143
  MANIFEST_PATH = Path(__file__).resolve().parent.parent / "tensors.txt"
144
 
145
  DEFAULT_ADDR_BITS = 16
 
146
  DEFAULT_MEM_BYTES = 1 << DEFAULT_ADDR_BITS
147
 
148
  MEMORY_PROFILES = {
@@ -166,8 +73,7 @@ def load_tensors(path: Path) -> Dict[str, torch.Tensor]:
166
 
167
 
168
  def load_file_metadata(path: Path) -> Dict[str, str]:
169
- """Read the safetensors header metadata so every save can carry it
170
- forward (signal_registry, weight_quantization, ...)."""
171
  with safe_open(str(path), framework="pt") as f:
172
  meta = f.metadata()
173
  return dict(meta) if meta else {}
@@ -264,12 +170,7 @@ def add_memory_write_cells(tensors: Dict[str, torch.Tensor], mem_bytes: int) ->
264
 
265
 
266
  def add_fetch_load_store_buffers(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
267
- """Add control buffers for fetch, load, store operations.
268
-
269
- Args:
270
- data_bits: Width of data bus (8/16/32)
271
- addr_bits: Width of address bus (determines instruction register width)
272
- """
273
  # Instruction register width = opcode (8) + operands (depends on arch)
274
  # For simplicity, IR width = max(16, addr_bits) to hold jump targets
275
  ir_bits = max(16, addr_bits)
@@ -283,13 +184,7 @@ def add_fetch_load_store_buffers(tensors: Dict[str, torch.Tensor], data_bits: in
283
 
284
 
285
  def add_full_adder(tensors: Dict[str, torch.Tensor], prefix: str) -> None:
286
- """Add a single full adder at the given prefix.
287
-
288
- Full adder structure:
289
- - ha1: first half adder (A XOR B for sum, A AND B for carry)
290
- - ha2: second half adder (ha1.sum XOR Cin for sum, ha1.sum AND Cin for carry)
291
- - carry_or: OR of ha1.carry and ha2.carry for final carry out
292
- """
293
  # XOR for ha1.sum (2-layer: OR + NAND -> AND)
294
  add_gate(tensors, f"{prefix}.ha1.sum.layer1.or", [1.0, 1.0], [-1.0])
295
  add_gate(tensors, f"{prefix}.ha1.sum.layer1.nand", [-1.0, -1.0], [1.0])
@@ -307,21 +202,7 @@ def add_full_adder(tensors: Dict[str, torch.Tensor], prefix: str) -> None:
307
 
308
 
309
  def add_expr_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
310
- """Add expression circuit for A + B × C (order of operations).
311
-
312
- Computes A + (B × C) where multiplication has higher precedence.
313
-
314
- Structure:
315
- - Stage 1: Multiply B × C using shift-add algorithm
316
- - 8 mask stages: mask[i] = B AND C[i] (8 AND gates each, shifted)
317
- - 7 accumulator adders to sum masked values
318
- - Stage 2: Add A to multiplication result (8-bit ripple carry)
319
-
320
- Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first, 8-bit each)
321
- Output: 8-bit result of A + (B × C), wrapping on overflow
322
-
323
- Total: 64 AND gates + 7×8 full adders (mul) + 8 full adders (add) = ~640 gates
324
- """
325
  prefix = "arithmetic.expr_add_mul"
326
 
327
  # Stage 1: Multiply B × C using shift-add
@@ -357,22 +238,7 @@ def add_expr_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
357
 
358
 
359
  def add_expr_paren_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
360
- """Add expression circuit for (A + B) × C (parenthetical override).
361
-
362
- Computes (A + B) × C where parentheses override normal precedence.
363
- Addition happens first, then multiplication.
364
-
365
- Structure:
366
- - Stage 1: Add A + B (8-bit ripple carry adder)
367
- - Stage 2: Multiply sum × C using shift-add algorithm
368
- - 8 mask stages: mask[i] = sum AND C[i] (8 AND gates each)
369
- - 7 accumulator adders to sum shifted masked values
370
-
371
- Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first, 8-bit each)
372
- Output: 8-bit result of (A + B) × C, wrapping on overflow
373
-
374
- Total: 8 full adders (add) + 64 AND gates + 56 full adders (mul) = ~640 gates
375
- """
376
  prefix = "arithmetic.expr_paren_add_mul"
377
 
378
  # Stage 1: Add A + B
@@ -392,21 +258,7 @@ def add_expr_paren_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
392
 
393
 
394
  def add_expr_paren(tensors: Dict[str, torch.Tensor]) -> None:
395
- """Add expression circuit for (A + B) × C (parenthetical grouping).
396
-
397
- Computes (A + B) × C where addition happens first due to parentheses.
398
-
399
- Structure:
400
- - Stage 1: Add A + B (8-bit ripple carry)
401
- - Stage 2: Multiply sum × C using shift-add algorithm
402
- - 8 mask stages: mask[i] = sum AND C[i] (8 AND gates each)
403
- - 7 accumulator adders to sum shifted masked values
404
-
405
- Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first, 8-bit each)
406
- Output: 8-bit result of (A + B) × C, wrapping on overflow
407
-
408
- Total: 8 full adders (add) + 64 AND gates + 56 full adders (mul) = ~640 gates
409
- """
410
  prefix = "arithmetic.expr_paren"
411
 
412
  # Stage 1: Add A + B
@@ -426,17 +278,7 @@ def add_expr_paren(tensors: Dict[str, torch.Tensor]) -> None:
426
 
427
 
428
  def add_add3(tensors: Dict[str, torch.Tensor]) -> None:
429
- """Add 3-operand 8-bit adder circuit.
430
-
431
- Computes A + B + C using two chained ripple-carry stages:
432
- - Stage 1: temp = A + B (8 full adders)
433
- - Stage 2: result = temp + C (8 full adders)
434
-
435
- Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first)
436
- Outputs: stage2.fa0-7.ha2.sum.layer2 (result bits), stage2.fa7.carry_or (overflow)
437
-
438
- Total: 16 full adders = 144 gates
439
- """
440
  # Stage 1: A + B -> temp
441
  for bit in range(8):
442
  add_full_adder(tensors, f"arithmetic.add3_8bit.stage1.fa{bit}")
@@ -447,14 +289,7 @@ def add_add3(tensors: Dict[str, torch.Tensor]) -> None:
447
 
448
 
449
  def add_shl_shr(tensors: Dict[str, torch.Tensor]) -> None:
450
- """Add SHL (shift left) and SHR (shift right) circuits.
451
-
452
- Identity gate: w=1, b=-1 -> H(x - 1) = x for x in {0,1}
453
- Zero gate: w=0, b=-1 -> H(-1) = 0
454
-
455
- SHL (MSB-first): out[i] = in[i+1] for i<7, out[7] = 0
456
- SHR (MSB-first): out[0] = 0, out[i] = in[i-1] for i>0
457
- """
458
  for bit in range(8):
459
  if bit < 7:
460
  add_gate(tensors, f"alu.alu8bit.shl.bit{bit}", [1.0], [-1.0])
@@ -469,18 +304,7 @@ def add_shl_shr(tensors: Dict[str, torch.Tensor]) -> None:
469
 
470
 
471
  def add_mul(tensors: Dict[str, torch.Tensor]) -> None:
472
- """Add 8-bit multiplication circuit.
473
-
474
- Produces low 8 bits of the 16-bit result.
475
-
476
- Structure:
477
- - 64 AND gates for partial products P[i][j] = A[i] AND B[j]
478
- - Uses existing ripple-carry adder components for summation
479
-
480
- The multiply method in ThresholdALU computes:
481
- 1. Partial products via these AND gates
482
- 2. Shift-add accumulation via existing 8-bit adder
483
- """
484
  # AND gates for partial products: P[i][j] = A[i] AND B[j]
485
  # These compute whether bit i of A and bit j of B are both 1
486
  for i in range(8):
@@ -489,19 +313,7 @@ def add_mul(tensors: Dict[str, torch.Tensor]) -> None:
489
 
490
 
491
  def add_div(tensors: Dict[str, torch.Tensor]) -> None:
492
- """Add 8-bit division circuit.
493
-
494
- Produces quotient (8 bits) and remainder (8 bits).
495
-
496
- Uses restoring division algorithm:
497
- - 8 iterations, each producing one quotient bit
498
- - Each iteration: compare, conditionally subtract, shift
499
-
500
- Structure:
501
- - 8 comparison gates (one per iteration)
502
- - 8 conditional subtraction stages
503
- - Uses existing comparator and subtractor components
504
- """
505
  # Comparison gates: check if (remainder << 1 | next_bit) >= divisor
506
  # Bit-cascaded ternary GE comparator per stage.
507
  for stage in range(8):
@@ -529,14 +341,7 @@ def add_div(tensors: Dict[str, torch.Tensor]) -> None:
529
 
530
 
531
  def add_inc_dec(tensors: Dict[str, torch.Tensor]) -> None:
532
- """Add INC and DEC circuits.
533
-
534
- INC: A + 1 using half adders with carry chain
535
- DEC: A - 1 using borrow chain (A + 255, two's complement of 1)
536
-
537
- For INC, we add 1 to the LSB and propagate carry.
538
- For DEC, we add 0xFF (two's complement of 1) or use borrow logic.
539
- """
540
  # INC: half adder chain starting with carry_in = 1
541
  # bit 7 (LSB): XOR with 1, carry = bit[7]
542
  # bit 6: XOR with carry, new_carry = bit[6] AND old_carry
@@ -563,12 +368,7 @@ def add_inc_dec(tensors: Dict[str, torch.Tensor]) -> None:
563
 
564
 
565
  def add_neg(tensors: Dict[str, torch.Tensor]) -> None:
566
- """Add NEG circuit (two's complement negation).
567
-
568
- NEG(A) = NOT(A) + 1 = ~A + 1
569
-
570
- Structure: NOT gates followed by INC-style adder.
571
- """
572
  for bit in range(8):
573
  # NOT gate for each bit
574
  add_gate(tensors, f"alu.alu8bit.neg.not.bit{bit}", [-1.0], [0.0])
@@ -585,14 +385,7 @@ def add_rol_ror(tensors: Dict[str, torch.Tensor]) -> None:
585
 
586
 
587
  def add_rol_ror_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
588
- """Add N-bit ROL and ROR circuits (rotate left/right).
589
-
590
- ROL: out[i] = in[i+1] for i<N-1, out[N-1] = in[0] (MSB wraps to LSB)
591
- ROR: out[0] = in[N-1], out[i] = in[i-1] for i>0 (LSB wraps to MSB)
592
-
593
- Args:
594
- bits: Data width (8, 16, 32, etc.)
595
- """
596
  # ROL: rotate left (toward MSB)
597
  for bit in range(bits):
598
  add_gate(tensors, f"alu.alu{bits}bit.rol.bit{bit}", [1.0], [-1.0])
@@ -603,19 +396,7 @@ def add_rol_ror_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
603
 
604
 
605
  def add_stack_ops(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
606
- """Add RET, PUSH, POP circuit components.
607
-
608
- These are higher-level operations that use memory read/write.
609
- We create the control logic gates.
610
-
611
- Args:
612
- data_bits: Width of data to push/pop (8/16/32)
613
- addr_bits: Width of stack pointer and return addresses
614
-
615
- RET: Pop return address from stack, jump to it
616
- PUSH: Decrement SP, write value to [SP]
617
- POP: Read value from [SP], increment SP
618
- """
619
  # SP decrement for PUSH (addr_bits wide)
620
  for bit in range(addr_bits):
621
  add_gate(tensors, f"control.push.sp_dec.bit{bit}.xor.layer1.or", [1.0, 1.0], [-1.0])
@@ -644,21 +425,7 @@ def add_stack_ops(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: i
644
 
645
 
646
  def add_conditional_jumps(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
647
- """Add conditional jump circuits (JZ, JNZ, JC, JNC, JP, JN, JV, JNV).
648
-
649
- Each conditional jump is a 2:1 MUX per address bit:
650
- - If flag is set: output = target_bit
651
- - If flag is clear: output = pc_bit
652
-
653
- Structure per bit:
654
- - not_sel: NOT(flag)
655
- - and_a: pc_bit AND NOT(flag)
656
- - and_b: target_bit AND flag
657
- - or: and_a OR and_b
658
-
659
- Args:
660
- addr_bits: Width of program counter / jump target
661
- """
662
  jump_types = ['jz', 'jnz', 'jc', 'jnc', 'jp', 'jn', 'jv', 'jnv']
663
 
664
  for jmp in jump_types:
@@ -675,19 +442,14 @@ def add_conditional_jumps(tensors: Dict[str, torch.Tensor], addr_bits: int) -> N
675
 
676
 
677
  def add_computer_decode(tensors: Dict[str, torch.Tensor]) -> None:
678
- """Instruction decode for the 8-bit computer as threshold gates: a 4-to-16
679
- opcode one-hot decoder (exact match on the instruction's opcode nibble). The
680
- runtime reads these one-hots to dispatch instead of slicing the word."""
681
  for opc in range(16):
682
  w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(4)]
683
  add_gate(tensors, f"control.decode.op{opc}", w, [-float(bin(opc).count("1"))])
684
 
685
 
686
  def add_computer_pcnext(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
687
- """PC sequencing for the 8-bit computer as threshold gates: PC+2 / PC+4
688
- increment chains and a priority mux selecting the next PC among PC+2 (ALU),
689
- PC+4 (load/store), the conditional-jump mux, and the direct target
690
- (JMP/CALL). Replaces the runtime's Python PC+2/PC+4 and jump wiring."""
691
  for name, start in (("pc2", 1), ("pc4", 2)):
692
  for bit in range(addr_bits):
693
  if bit < start:
@@ -707,17 +469,7 @@ def add_computer_pcnext(tensors: Dict[str, torch.Tensor], addr_bits: int) -> Non
707
 
708
 
709
  def add_status_flags(tensors: Dict[str, torch.Tensor], data_bits: int) -> None:
710
- """Add status flag computation circuits (Z, N, C, V).
711
-
712
- Args:
713
- data_bits: Width of ALU data (8/16/32)
714
-
715
- Flags:
716
- - Z (Zero): NOR of all result bits (1 if result == 0)
717
- - N (Negative): Copy of MSB (sign bit)
718
- - C (Carry): Carry out from adder (external input)
719
- - V (Overflow): XOR of carry into and out of MSB (signed overflow)
720
- """
721
  # Z flag: NOR of all bits (result == 0)
722
  # Single threshold gate: fires if sum of all bits < 1
723
  add_gate(tensors, "flags.zero", [-1.0] * data_bits, [0.0])
@@ -741,14 +493,7 @@ def add_barrel_shifter(tensors: Dict[str, torch.Tensor]) -> None:
741
 
742
 
743
  def add_barrel_shifter_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
744
- """Add N-bit barrel shifter circuit.
745
-
746
- Shifts input by 0 to (bits-1) positions based on ceil(log2(bits))-bit shift amount.
747
- Uses layers of 2:1 muxes controlled by shift amount bits.
748
-
749
- Args:
750
- bits: Data width (8, 16, 32, etc.)
751
- """
752
  import math
753
  num_layers = max(1, math.ceil(math.log2(bits)))
754
 
@@ -769,21 +514,7 @@ def add_priority_encoder(tensors: Dict[str, torch.Tensor]) -> None:
769
 
770
 
771
  def add_priority_encoder_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
772
- """Add N-bit priority encoder circuit.
773
-
774
- Finds the position of the highest set bit (0 to bits-1).
775
- Position 0 = MSB (highest priority), Position bits-1 = LSB (lowest priority).
776
- Output is ceil(log2(bits))-bit index + valid flag.
777
-
778
- Circuit structure:
779
- 1. any_higher{pos}: OR of bits 0 to pos-1 (all higher-priority positions)
780
- 2. is_highest{pos}: bit[pos] AND NOT(any_higher{pos})
781
- 3. out{bit}: OR of is_highest{pos} for positions where (pos >> bit) & 1
782
- 4. valid: OR of all input bits
783
-
784
- Args:
785
- bits: Input width (8, 16, 32, etc.)
786
- """
787
  import math
788
  out_bits = max(1, math.ceil(math.log2(bits)))
789
  prefix = f"combinational.priorityencoder{bits}"
@@ -827,31 +558,7 @@ def add_bit_cascade_compare(
827
  out_le: str,
828
  out_eq: str,
829
  ) -> None:
830
- """Generic ternary-only N-bit comparator.
831
-
832
- Inputs are two N-bit values A and B in MSB-first order. The structure
833
- produces unsigned-magnitude GT, LT, GE, LE, EQ outputs using only
834
- weights in {-1, 0, 1} and integer biases.
835
-
836
- Per-bit primitives (i = 0 is the MSB):
837
- {cmp_prefix}.bit{i}.gt A[i] AND NOT B[i] weights [1, -1], bias -1
838
- {cmp_prefix}.bit{i}.lt NOT A[i] AND B[i] weights [-1, 1], bias -1
839
- {cmp_prefix}.bit{i}.eq.layer1.and A[i] AND B[i]
840
- {cmp_prefix}.bit{i}.eq.layer1.nor NOR(A[i], B[i])
841
- {cmp_prefix}.bit{i}.eq XNOR via OR of layer1 outputs
842
-
843
- Cascade (linear chain from MSB to LSB):
844
- {cmp_prefix}.cascade.eq_prefix.bit{i} AND of eq[0..i-1] (i in 1..N-1)
845
- {cmp_prefix}.cascade.gt.bit{i} eq_prefix[i] AND gt[i]
846
- {cmp_prefix}.cascade.lt.bit{i} eq_prefix[i] AND lt[i]
847
-
848
- Final outputs:
849
- out_gt = OR of (gt[0], cascade.gt.bit{1..N-1})
850
- out_lt = OR of (lt[0], cascade.lt.bit{1..N-1})
851
- out_eq = AND of all eq[i]
852
- out_ge = NOT(out_lt)
853
- out_le = NOT(out_gt)
854
- """
855
  for i in range(bits):
856
  # per-bit GT: A[i] AND NOT B[i] -> H(A - B - 1)
857
  add_gate(tensors, f"{cmp_prefix}.bit{i}.gt", [1.0, -1.0], [-1.0])
@@ -891,11 +598,7 @@ def add_bit_cascade_compare(
891
 
892
 
893
  def add_comparators(tensors: Dict[str, torch.Tensor]) -> None:
894
- """Add 8-bit comparator circuits (GT, LT, GE, LE, EQ) using bit-cascade.
895
-
896
- Inputs are 8 bits of A then 8 bits of B in MSB-first order. The
897
- underlying bit-cascade produces only ternary {-1, 0, 1} weights.
898
- """
899
  add_bit_cascade_compare(
900
  tensors,
901
  cmp_prefix="arithmetic.cmp8bit",
@@ -909,30 +612,14 @@ def add_comparators(tensors: Dict[str, torch.Tensor]) -> None:
909
 
910
 
911
  def add_ripple_carry_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
912
- """Add N-bit ripple carry adder circuit.
913
-
914
- Creates a chain of full adders for N-bit addition.
915
- Works for 8, 16, or 32 bits.
916
-
917
- Inputs: $a[0..N-1], $b[0..N-1] (MSB-first)
918
- Outputs: fa0-fa{N-1} sum bits, fa{N-1}.carry_or for overflow
919
- """
920
  prefix = f"arithmetic.ripplecarry{bits}bit"
921
  for bit in range(bits):
922
  add_full_adder(tensors, f"{prefix}.fa{bit}")
923
 
924
 
925
  def add_sub_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
926
- """Add N-bit subtractor circuit (A - B).
927
-
928
- Uses two's complement: A - B = A + (~B) + 1
929
-
930
- Structure:
931
- - NOT gates for each bit of B
932
- - N-bit ripple carry adder with carry_in = 1
933
-
934
- The carry_in=1 is handled by the adder's fa0 having cin=#1 instead of #0.
935
- """
936
  prefix = f"arithmetic.sub{bits}bit"
937
 
938
  for bit in range(bits):
@@ -943,10 +630,7 @@ def add_sub_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
943
 
944
 
945
  def add_comparators_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
946
- """Add N-bit comparator circuits (GT, LT, GE, LE, EQ) via bit-cascade.
947
-
948
- All weights are in {-1, 0, 1}. Inputs are A bits then B bits, MSB-first.
949
- """
950
  add_bit_cascade_compare(
951
  tensors,
952
  cmp_prefix=f"arithmetic.cmp{bits}bit",
@@ -960,27 +644,14 @@ def add_comparators_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
960
 
961
 
962
  def add_mul_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
963
- """Add N-bit multiplication circuit.
964
-
965
- Produces low N bits of the 2N-bit result.
966
-
967
- Structure:
968
- - N*N AND gates for partial products P[i][j] = A[i] AND B[j]
969
- - Shift-add accumulation using existing adder circuits
970
-
971
- For 32-bit: 1024 AND gates for partial products.
972
- """
973
  for i in range(bits):
974
  for j in range(bits):
975
  add_gate(tensors, f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [1.0, 1.0], [-2.0])
976
 
977
 
978
  def add_div_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
979
- """Add N-bit division circuit.
980
-
981
- Uses restoring division algorithm with N iterations. Each stage's
982
- comparator (rem >= div) is a ternary bit-cascade GE.
983
- """
984
  for stage in range(bits):
985
  add_bit_cascade_compare(
986
  tensors,
@@ -1002,10 +673,7 @@ def add_div_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
1002
 
1003
 
1004
  def add_bitwise_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
1005
- """Add N-bit bitwise operation circuits (AND, OR, XOR, NOT).
1006
-
1007
- These are simply N copies of the 1-bit gates.
1008
- """
1009
  for bit in range(bits):
1010
  add_gate(tensors, f"alu.alu{bits}bit.and.bit{bit}", [1.0, 1.0], [-2.0])
1011
 
@@ -1022,11 +690,7 @@ def add_bitwise_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
1022
 
1023
 
1024
  def add_shift_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
1025
- """Add N-bit shift circuits (SHL, SHR by 1 position).
1026
-
1027
- SHL: out[i] = in[i+1] for i<N-1, out[N-1] = 0
1028
- SHR: out[0] = 0, out[i] = in[i-1] for i>0
1029
- """
1030
  for bit in range(bits):
1031
  if bit < bits - 1:
1032
  add_gate(tensors, f"alu.alu{bits}bit.shl.bit{bit}", [1.0], [-1.0])
@@ -1067,19 +731,7 @@ def add_neg_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
1067
 
1068
 
1069
  def add_float16_core(tensors: Dict[str, torch.Tensor]) -> None:
1070
- """Add float16 core circuits (unpack, pack, classify).
1071
-
1072
- IEEE 754 half-precision format (16 bits):
1073
- - Bit 15: Sign (0=positive, 1=negative)
1074
- - Bits 14-10: Exponent (5 bits, bias=15)
1075
- - Bits 9-0: Mantissa/fraction (10 bits, implicit leading 1 for normalized)
1076
-
1077
- Special values:
1078
- - Zero: exp=0, frac=0
1079
- - Subnormal: exp=0, frac≠0
1080
- - Infinity: exp=31, frac=0
1081
- - NaN: exp=31, frac≠0
1082
- """
1083
  prefix = "float16"
1084
 
1085
  # Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
@@ -1103,28 +755,7 @@ def add_float16_core(tensors: Dict[str, torch.Tensor]) -> None:
1103
 
1104
  def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
1105
  exp_bits: int, frac_bits: int) -> None:
1106
- """Add a complete float addition/subtraction pipeline.
1107
-
1108
- Self-contained circuit; external inputs are the raw operand words
1109
- $a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
1110
- (NaN, infinities, signed zeros, opposite-sign infinities -> NaN, exact
1111
- cancellation -> +0), subnormal operands and gradual-underflow subnormal
1112
- results, round-to-nearest-even for the mantissa. A zero operand passes the
1113
- other operand through verbatim.
1114
-
1115
- Datapath: payload comparison picks the larger-magnitude operand, swap
1116
- muxes route it to L; the smaller mantissa is aligned right through a
1117
- capped barrel shifter over an F+3-bit round+guard frame with a sticky
1118
- collector; add and subtract chains run in parallel (the subtract chain
1119
- takes the sticky as a borrow-in so its round bit is exact) and the sign
1120
- agreement selects; a leading-zero counter drives the left renormalize
1121
- barrel; guard/round/sticky then drive a round-to-nearest-even increment
1122
- on the fraction with a carry into the exponent; two E+2-bit exponent
1123
- chains compute exp_L + 1 - nlz plus the rounding carry; and a one-hot
1124
- specials network selects the output word.
1125
-
1126
- Rounding: round-to-nearest-even (RNE), matching multiply and divide.
1127
- """
1128
  E, F = exp_bits, frac_bits
1129
  P = E + F # payload width
1130
  FRAME = F + 3 # mantissa + guard + round
@@ -1341,21 +972,7 @@ def add_float16_add(tensors: Dict[str, torch.Tensor]) -> None:
1341
 
1342
  def add_float_mul(tensors: Dict[str, torch.Tensor], family: str,
1343
  exp_bits: int, frac_bits: int) -> None:
1344
- """Add a complete float multiplication pipeline.
1345
-
1346
- Self-contained circuit; external inputs are the raw operand words
1347
- $a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
1348
- (NaN, infinities, signed zeros), subnormal operands and gradual-underflow
1349
- subnormal results, round-to-nearest-even for the mantissa.
1350
-
1351
- Datapath: (F+1)x(F+1) partial products accumulated shift-add into the
1352
- 2F+2-bit product; a leading-zero count and an E+2-bit exponent chain give
1353
- exp_base = exp_a + exp_b - bias and a signed clamp min(nlz, exp_base); the
1354
- product is right-shifted by (F+1) - that clamp (one shifter that normalizes
1355
- normal results, denormalizes subnormal ones, and drives deeply underflowing
1356
- products to zero), then round-to-nearest-even and a one-hot specials network
1357
- produce the output word.
1358
- """
1359
  E, F = exp_bits, frac_bits
1360
  prefix = f"{family}.mul"
1361
 
@@ -1498,22 +1115,7 @@ def add_float16_mul(tensors: Dict[str, torch.Tensor]) -> None:
1498
 
1499
  def add_float_div(tensors: Dict[str, torch.Tensor], family: str,
1500
  exp_bits: int, frac_bits: int) -> None:
1501
- """Add a complete float division pipeline.
1502
-
1503
- Self-contained circuit; external inputs are the raw operand words
1504
- $a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
1505
- (NaN, infinities, signed zeros, x/0 -> inf, 0/0 and inf/inf -> NaN),
1506
- subnormal operands and gradual-underflow subnormal results,
1507
- round-to-nearest-even for the mantissa.
1508
-
1509
- Datapath: each operand mantissa is pre-normalized (a leading-zero count
1510
- drives a left-barrel so the leading 1 sits at bit F, and na = eexp - lz);
1511
- F+3 restoring-division stages produce the quotient; a one-bit normalize mux
1512
- aligns it on the integer quotient bit; two E+2-bit exponent chains give
1513
- er = na - nb + bias - 1 + q0; the aligned mantissa frame is right-shifted by
1514
- max(0, 1 - er) to denormalize subnormal quotients; then round-to-nearest-even
1515
- and a one-hot specials network produce the output word.
1516
- """
1517
  E, F = exp_bits, frac_bits
1518
  prefix = f"{family}.div"
1519
  NSTAGE = F + 3
@@ -1686,21 +1288,7 @@ def add_float16_div(tensors: Dict[str, torch.Tensor]) -> None:
1686
 
1687
  def add_float_fma(tensors: Dict[str, torch.Tensor], family: str,
1688
  exp_bits: int, frac_bits: int) -> None:
1689
- """Fused multiply-add: round(a*b + c) with a SINGLE rounding (F-extension).
1690
-
1691
- Self-contained; external inputs are the raw operand words $a/$b/$c
1692
- (MSB-first). Contract: exact IEEE specials (NaN, infinities, inf*0 and
1693
- inf-inf -> NaN, signed zeros), subnormal operands and gradual-underflow
1694
- subnormal results, round-to-nearest-even, single rounding of a*b+c.
1695
-
1696
- Datapath: each mantissa is pre-normalized (leading 1 at bit F, na=eexp-lz);
1697
- the (F+1)x(F+1) product Mp (2F+2 bits) and the addend Mc are right-shifted by
1698
- sticky barrel shifters into a common 3(F+1)+4-bit field anchored at
1699
- max(product,c) exponent; a signed magnitude add (with a borrow when the
1700
- shifted-out subtrahend leaves a tail) forms the sum; a leading-zero count and
1701
- normalize give the significand, one round-to-nearest-even rounds it, and a
1702
- one-hot specials network packs the result.
1703
- """
1704
  E, F = exp_bits, frac_bits
1705
  W = 1 + E + F
1706
  p = F + 1
@@ -1981,18 +1569,7 @@ def add_float_fma(tensors: Dict[str, torch.Tensor], family: str,
1981
 
1982
  def add_float_cmp(tensors: Dict[str, torch.Tensor], family: str,
1983
  exp_bits: int, frac_bits: int) -> None:
1984
- """Add a complete IEEE 754 comparison network (EQ, LT, LE, GT, GE).
1985
-
1986
- Self-contained circuit: external inputs are the raw operand words
1987
- $a[0..W-1], $b[0..W-1] (MSB-first; bit 0 is the sign, bits 1..E the
1988
- exponent, the rest the fraction). Composition:
1989
-
1990
- 1. NaN detection gates every result (any comparison with NaN is false).
1991
- 2. both_zero makes +0 == -0 (and suppresses the mixed-sign LT/GT).
1992
- 3. Same signs: IEEE encodings order like sign-magnitude integers, so the
1993
- payload magnitude comparator decides, inverted for negatives.
1994
- 4. Different signs: the negative operand is smaller (unless both zero).
1995
- """
1996
  prefix = f"{family}.cmp"
1997
  payload = exp_bits + frac_bits
1998
 
@@ -2080,13 +1657,7 @@ def add_float16_cmp(tensors: Dict[str, torch.Tensor]) -> None:
2080
 
2081
 
2082
  def add_float32_core(tensors: Dict[str, torch.Tensor]) -> None:
2083
- """Add float32 core circuits (unpack, pack, classify).
2084
-
2085
- IEEE 754 single-precision format (32 bits):
2086
- - Bit 31: Sign
2087
- - Bits 30-23: Exponent (8 bits, bias=127)
2088
- - Bits 22-0: Mantissa (23 bits, implicit leading 1)
2089
- """
2090
  prefix = "float32"
2091
 
2092
  # Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
@@ -2125,13 +1696,7 @@ def add_float32_div(tensors: Dict[str, torch.Tensor]) -> None:
2125
  add_float_div(tensors, "float32", 8, 23)
2126
 
2127
  def update_manifest(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int, mem_bytes: int) -> None:
2128
- """Update manifest metadata tensors.
2129
-
2130
- Args:
2131
- data_bits: ALU/register width (8/16/32)
2132
- addr_bits: Address bus width (determines memory size)
2133
- mem_bytes: Memory size in bytes (2^addr_bits)
2134
- """
2135
  tensors["manifest.data_bits"] = torch.tensor([float(data_bits)], dtype=torch.float32)
2136
  tensors["manifest.addr_bits"] = torch.tensor([float(addr_bits)], dtype=torch.float32)
2137
  tensors["manifest.memory_bytes"] = torch.tensor([float(mem_bytes)], dtype=torch.float32)
@@ -2228,16 +1793,7 @@ def infer_ripplecarry_inputs(gate: str, prefix: str, bits: int, reg: SignalRegis
2228
 
2229
 
2230
  def infer_expr_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
2231
- """Infer inputs for A + B × C expression circuit (order of operations).
2232
-
2233
- Circuit structure:
2234
- - Mask stage: mask.s[stage].b[bit] = B[bit] AND C[stage]
2235
- - Accumulator stages 1-7: acc.s[stage] = acc.s[stage-1] + (mask.s[stage] << stage)
2236
- - Final add: result = A + acc.s7
2237
-
2238
- Bit ordering: MSB-first externally, LSB-first internally (fa0 = LSB, fa7 = MSB)
2239
- - $x[7] = bit 0 (LSB), $x[0] = bit 7 (MSB)
2240
- """
2241
  prefix = "arithmetic.expr_add_mul"
2242
 
2243
  # Register all inputs
@@ -2348,15 +1904,7 @@ def infer_expr_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
2348
 
2349
 
2350
  def infer_expr_paren_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
2351
- """Infer inputs for (A + B) × C expression circuit (parenthetical override).
2352
-
2353
- Circuit structure:
2354
- - Add stage: sum = A + B
2355
- - Mask stage: mask.s[stage].b[bit] = sum[bit] AND C[stage]
2356
- - Accumulator stages 1-7: acc.s[stage] = acc.s[stage-1] + (mask.s[stage] << stage)
2357
-
2358
- Bit ordering: MSB-first externally, LSB-first internally (fa0 = LSB, fa7 = MSB)
2359
- """
2360
  prefix = "arithmetic.expr_paren_add_mul"
2361
 
2362
  # Register all inputs
@@ -2463,15 +2011,7 @@ def infer_expr_paren_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]
2463
 
2464
 
2465
  def infer_expr_paren_inputs(gate: str, reg: SignalRegistry) -> List[int]:
2466
- """Infer inputs for (A + B) × C expression circuit (parenthetical grouping).
2467
-
2468
- Circuit structure:
2469
- - Add stage: sum = A + B
2470
- - Mask stage: mask.s[stage].b[bit] = sum[bit] AND C[stage]
2471
- - Accumulator stages 1-7: acc.s[stage] = acc.s[stage-1] + (mask.s[stage] << stage)
2472
-
2473
- Bit ordering: MSB-first externally, LSB-first internally (fa0 = LSB, fa7 = MSB)
2474
- """
2475
  prefix = "arithmetic.expr_paren"
2476
 
2477
  # Register all inputs
@@ -3117,11 +2657,7 @@ def infer_combinational_inputs(gate: str, reg: SignalRegistry, tensors: Dict[str
3117
 
3118
  def _infer_bit_cascade(gate: str, reg: SignalRegistry, cmp_prefix: str,
3119
  a_template: str, b_template: str) -> List[int] | None:
3120
- """If `gate` is part of the bit-cascade comparator at `cmp_prefix`, return
3121
- its inputs. `a_template` / `b_template` are format strings like
3122
- "$a[{}]" / "$b[{}]" that produce the per-bit input signal names. Returns
3123
- None if `gate` isn't a member of this cascade.
3124
- """
3125
  if not gate.startswith(cmp_prefix + "."):
3126
  return None
3127
  suffix = gate[len(cmp_prefix) + 1:]
@@ -3161,10 +2697,7 @@ def _infer_bit_cascade(gate: str, reg: SignalRegistry, cmp_prefix: str,
3161
  def _infer_compare_final(gate: str, reg: SignalRegistry, cmp_prefix: str,
3162
  out_gt: str, out_lt: str, out_ge: str, out_le: str,
3163
  out_eq: str, bits: int) -> List[int] | None:
3164
- """Inputs for the final outputs of an add_bit_cascade_compare emit:
3165
- out_gt/out_lt are N-input ORs; out_eq is N-input AND; ge/le are
3166
- NOT-then-buffer chains over lt/gt.
3167
- """
3168
  if gate == out_gt:
3169
  return ([reg.register(f"{cmp_prefix}.bit0.gt")]
3170
  + [reg.register(f"{cmp_prefix}.cascade.gt.bit{i}") for i in range(1, bits)])
@@ -3186,12 +2719,7 @@ def _infer_compare_final(gate: str, reg: SignalRegistry, cmp_prefix: str,
3186
 
3187
  def infer_float_cmp_inputs(gate: str, reg: SignalRegistry, family: str,
3188
  exp_bits: int, frac_bits: int) -> Optional[List[int]]:
3189
- """Complete wiring for the self-contained float comparison network.
3190
-
3191
- External inputs are the raw operand words $a[0..W-1] / $b[0..W-1],
3192
- MSB-first: bit 0 is the sign, bits 1..E the exponent, the rest the
3193
- fraction. Field extraction is input wiring, not gates.
3194
- """
3195
  prefix = f"{family}.cmp"
3196
  if gate != prefix and not gate.startswith(prefix + "."):
3197
  return None
@@ -3306,8 +2834,7 @@ def infer_float_cmp_inputs(gate: str, reg: SignalRegistry, family: str,
3306
 
3307
  def _fa_member_inputs(sub: str, fa_prefix: str, a_sig: int, b_sig: int,
3308
  cin_sig: int, reg: SignalRegistry) -> Optional[List[int]]:
3309
- """Wiring for one gate of an add_full_adder cell, given the cell's
3310
- operand/carry signal IDs. `sub` is the gate suffix inside the cell."""
3311
  if sub == "ha1.sum.layer1.or" or sub == "ha1.sum.layer1.nand":
3312
  return [a_sig, b_sig]
3313
  if sub == "ha1.sum.layer2":
@@ -3330,13 +2857,7 @@ def _fa_member_inputs(sub: str, fa_prefix: str, a_sig: int, b_sig: int,
3330
 
3331
  def infer_float_mul_inputs(gate: str, reg: SignalRegistry, family: str,
3332
  exp_bits: int, frac_bits: int) -> Optional[List[int]]:
3333
- """Complete wiring for the self-contained float multiplication pipeline.
3334
-
3335
- Operand words $a/$b are MSB-first. Internally the mantissa and exponent
3336
- signals are LSB-first: mantissa bit k of `a` is $a[W-1-k] for k < F with
3337
- the implicit leading one (index F) wired to #1; exponent bit k is
3338
- $a[E-k]. The accumulated product bit k is acc.s{F-1}.fa{k}'s sum.
3339
- """
3340
  E, F = exp_bits, frac_bits
3341
  W = 1 + E + F
3342
  prefix = f"{family}.mul"
@@ -3613,14 +3134,7 @@ def infer_float_mul_inputs(gate: str, reg: SignalRegistry, family: str,
3613
 
3614
  def infer_float_div_inputs(gate: str, reg: SignalRegistry, family: str,
3615
  exp_bits: int, frac_bits: int) -> Optional[List[int]]:
3616
- """Complete wiring for the self-contained float division pipeline.
3617
-
3618
- Stage 0 compares the dividend mantissa against the divisor directly;
3619
- every later stage shifts the previous remainder left by one (the
3620
- shifted-out top bit forces the quotient bit through the stage OR) and
3621
- conditionally subtracts. The quotient bit of stage s is
3622
- mant_div.stage{s}.q; the remainder bits are the stage mux outputs.
3623
- """
3624
  E, F = exp_bits, frac_bits
3625
  W = 1 + E + F
3626
  prefix = f"{family}.div"
@@ -5009,15 +4523,7 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
5009
 
5010
 
5011
  def add_subleq_core(tensors: Dict[str, torch.Tensor]) -> None:
5012
- """Add the SUBLEQ one-instruction datapath.
5013
-
5014
- The machine: M[B] -= M[A]; branch to C when the result is <= 0 (two's
5015
- complement). External inputs, LSB-first: $a (subtrahend M[A]), $b
5016
- (minuend M[B]), $pc (current PC), $c (branch target). Outputs: the new
5017
- M[B] on sub.fa{k} sums, and the next PC on pc_mux.bit{k}.or. The one
5018
- architectural decision -- branch or fall through -- is the single
5019
- threshold neuron subleq.leq over (sign, is-zero).
5020
- """
5021
  for k in range(8):
5022
  add_gate(tensors, f"subleq.sub.not_a.bit{k}", [-1.0], [0.0])
5023
  add_full_adder(tensors, f"subleq.sub.fa{k}")
@@ -5085,12 +4591,7 @@ def infer_subleq_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
5085
 
5086
 
5087
  def infer_rv32_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
5088
- """Complete wiring for the rv32-specific sub-circuits: the 64-bit MULH
5089
- accumulator (fed by the shared 32-bit partial products), the SRA select
5090
- muxes, the NEUR neuron datapath, and the dual-issue hazard comparators.
5091
- All operand-side ports are external ($rv32_...); internal chains resolve
5092
- to gate outputs, so each sub-circuit's netlist is recoverable in full.
5093
- """
5094
  if not gate.startswith("rv32."):
5095
  return None
5096
 
@@ -5244,10 +4745,7 @@ def infer_rv32_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
5244
 
5245
 
5246
  def _infer_threshold_computer_bit_cascade(gate: str, reg: SignalRegistry) -> List[int] | None:
5247
- """Match every bit-cascade location used in this codebase: integer
5248
- comparators, integer division stage cmps, float magnitude comparators,
5249
- float exp_cmp, and float division mantissa cmps.
5250
- """
5251
  # Integer arithmetic.cmp{N}bit.* + the public greaterthan/lessthan/etc.
5252
  m = re.match(r"^arithmetic\.cmp(\d+)bit\..+", gate)
5253
  if m:
@@ -5315,16 +4813,7 @@ _ROUTING_TABLE_CACHE: Dict[str, Any] | None = None
5315
 
5316
 
5317
  def _load_routing_table() -> Dict[str, Any]:
5318
- """Load `routing/routing.json` once; on missing file, return {}.
5319
-
5320
- The JSON maps each top-level circuit name (e.g. ``arithmetic.multiplier8x8``)
5321
- to a dict with at least ``inputs`` (top-level external ports) and
5322
- ``internal`` (gate-suffix -> list-of-source-signal-name). The source
5323
- signal names are either external port references (``$a[3]``), constants
5324
- (``#0`` / ``#1``), or relative gate names within the same circuit
5325
- (e.g. ``pp.r0.c3``). Anchored cross-circuit references are not
5326
- expressed here; circuits in the table are assumed self-contained.
5327
- """
5328
  global _ROUTING_TABLE_CACHE
5329
  if _ROUTING_TABLE_CACHE is not None:
5330
  return _ROUTING_TABLE_CACHE
@@ -5342,16 +4831,7 @@ def _infer_from_routing_table(
5342
  gate: str, reg: SignalRegistry,
5343
  known_gate_suffixes: Optional[Set[str]] = None,
5344
  ) -> Optional[List[int]]:
5345
- """Resolve a gate's inputs by consulting routing.json.
5346
-
5347
- ``known_gate_suffixes`` is the set of gate-name suffixes that actually
5348
- exist in the loaded safetensors (within the same circuit prefix).
5349
- When the routing JSON names a producer like ``stage0.bit0.ha2.sum``
5350
- but that suffix isn't a real gate (only ``stage0.bit0.ha2.sum.layer2``
5351
- is), this set is consulted to redirect the reference to the canonical
5352
- final-layer signal. Returns None when the gate's circuit isn't
5353
- covered or its suffix isn't in the routing's ``internal`` map.
5354
- """
5355
  table = _load_routing_table()
5356
  if not table:
5357
  return None
@@ -5451,7 +4931,7 @@ def infer_inputs_for_gate(gate: str, reg: SignalRegistry, tensors: Dict[str, tor
5451
  return fc
5452
  # Bit-cascade comparators (integer + float + div) and ternary modular detectors.
5453
  # These are the gates emitted by add_bit_cascade_compare and the ternary
5454
- # modular rebuild in quantize.py; cover them up front so they don't fall
5455
  # through to the generic per-family handlers below.
5456
  bc = _infer_threshold_computer_bit_cascade(gate, reg)
5457
  if bc is not None:
@@ -6121,8 +5601,7 @@ def cmd_alu(args) -> None:
6121
 
6122
 
6123
  def cmd_subleq(args) -> None:
6124
- """Build the standalone SUBLEQ-8 machine file from scratch: 256 B of
6125
- packed memory circuits plus the one-instruction datapath, fully wired."""
6126
  out = args.model if args.model else MODEL_DIR / "variants" / "neural_subleq8.safetensors"
6127
  print("=" * 60)
6128
  print(" BUILD SUBLEQ-8 (one-instruction threshold machine)")
@@ -6153,10 +5632,7 @@ def cmd_subleq(args) -> None:
6153
 
6154
 
6155
  def add_rv32_extras(tensors: Dict[str, torch.Tensor]) -> None:
6156
- """RV32-specific circuits beyond the shared families: the full 64-bit
6157
- product accumulator for MULH*, arithmetic-right-shift select muxes,
6158
- the NEUR neuron-evaluation datapath, and dual-issue hazard comparators.
6159
- """
6160
  # 64-bit shift-add accumulator over the 32x32 partial products.
6161
  for stage in range(31):
6162
  for bit in range(64):
@@ -6195,9 +5671,11 @@ def add_rv32_extras(tensors: Dict[str, torch.Tensor]) -> None:
6195
  RV32_OPCLASS = {0x33: "op", 0x13: "op_imm", 0x03: "load", 0x23: "store",
6196
  0x63: "branch", 0x37: "lui", 0x17: "auipc", 0x6F: "jal",
6197
  0x67: "jalr", 0x73: "system", 0x53: "fp", 0x07: "flw", 0x27: "fsw"}
 
6198
  # Which opcode classes select each immediate format.
6199
  RV32_USE = {"I": ["op_imm", "load", "jalr", "flw"], "S": ["store", "fsw"],
6200
  "B": ["branch"], "U": ["lui", "auipc"], "J": ["jal"]}
 
6201
  RV32_IMM_FMTS = ("I", "S", "B", "U", "J")
6202
 
6203
 
@@ -6235,10 +5713,7 @@ def _rv32_imm_src(fmt: str, k: int):
6235
 
6236
 
6237
  def add_rv32_decode(tensors: Dict[str, torch.Tensor]) -> None:
6238
- """Instruction decode as threshold gates: opcode-class one-hots (an exact
6239
- 7-bit match on the instruction's opcode field) and the sign-extended
6240
- immediate for the active format, muxed from the raw instruction word. The
6241
- runtime reads these gate outputs instead of slicing the word in Python."""
6242
  for opc, name in RV32_OPCLASS.items():
6243
  w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(7)]
6244
  add_gate(tensors, f"rv32.decode.is_{name}", w, [-float(bin(opc).count("1"))])
@@ -6254,10 +5729,7 @@ def add_rv32_decode(tensors: Dict[str, torch.Tensor]) -> None:
6254
 
6255
 
6256
  def add_rv32_pcnext(tensors: Dict[str, torch.Tensor]) -> None:
6257
- """PC sequencing as threshold gates: a two-level mux selecting the next PC
6258
- among PC+4, PC+imm (branch/jal), and (rs1+imm)&~1 (jalr). The three
6259
- candidates are the gate adder's outputs; this network chooses between them
6260
- from the decode's control signals rather than a Python if/elif."""
6261
  for k in range(32):
6262
  # m1 = sel_target ? pcimm : pc4
6263
  add_gate(tensors, f"rv32.pcnext.m1.bit{k}.not_sel", [-1.0], [0.0])
@@ -6272,9 +5744,7 @@ def add_rv32_pcnext(tensors: Dict[str, torch.Tensor]) -> None:
6272
 
6273
 
6274
  def cmd_rv32(args) -> None:
6275
- """Build the standalone RV32 threshold processor file from scratch:
6276
- 64 KB packed memory, the shared 32-bit circuit families, the composed
6277
- float32 pipelines as the FPU, and the rv32-specific extras."""
6278
  out = args.model if args.model else MODEL_DIR / "variants" / "neural_rv32.safetensors"
6279
  print("=" * 60)
6280
  print(" BUILD NEURAL_RV32 (RV32IM + F-subset threshold processor)")
@@ -6458,6 +5928,528 @@ Usage (note: options must come BEFORE subcommand):
6458
  else:
6459
  parser.print_help()
6460
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6461
 
6462
  if __name__ == "__main__":
6463
- main()
 
 
 
 
 
1
+ """Builds the threshold computers, quantizes them, and builds every named variant."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from __future__ import annotations
3
 
4
  import argparse
5
  import json
6
+ import os
7
  import re
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import time
12
  from pathlib import Path
13
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
14
 
15
  import torch
16
  from safetensors import safe_open
 
45
 
46
 
47
  MODEL_PATH = MODEL_DIR / "neural_computer8.safetensors"
48
+
49
  MANIFEST_PATH = Path(__file__).resolve().parent.parent / "tensors.txt"
50
 
51
  DEFAULT_ADDR_BITS = 16
52
+
53
  DEFAULT_MEM_BYTES = 1 << DEFAULT_ADDR_BITS
54
 
55
  MEMORY_PROFILES = {
 
73
 
74
 
75
  def load_file_metadata(path: Path) -> Dict[str, str]:
76
+ """Read the safetensors header metadata so every save carries it forward."""
 
77
  with safe_open(str(path), framework="pt") as f:
78
  meta = f.metadata()
79
  return dict(meta) if meta else {}
 
170
 
171
 
172
  def add_fetch_load_store_buffers(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
173
+ """Add control buffers for fetch, load, store operations."""
 
 
 
 
 
174
  # Instruction register width = opcode (8) + operands (depends on arch)
175
  # For simplicity, IR width = max(16, addr_bits) to hold jump targets
176
  ir_bits = max(16, addr_bits)
 
184
 
185
 
186
  def add_full_adder(tensors: Dict[str, torch.Tensor], prefix: str) -> None:
187
+ """Add a single full adder at the given prefix."""
 
 
 
 
 
 
188
  # XOR for ha1.sum (2-layer: OR + NAND -> AND)
189
  add_gate(tensors, f"{prefix}.ha1.sum.layer1.or", [1.0, 1.0], [-1.0])
190
  add_gate(tensors, f"{prefix}.ha1.sum.layer1.nand", [-1.0, -1.0], [1.0])
 
202
 
203
 
204
  def add_expr_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
205
+ """Add expression circuit for A + B × C (order of operations)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  prefix = "arithmetic.expr_add_mul"
207
 
208
  # Stage 1: Multiply B × C using shift-add
 
238
 
239
 
240
  def add_expr_paren_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
241
+ """Add expression circuit for (A + B) × C (parenthetical override)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  prefix = "arithmetic.expr_paren_add_mul"
243
 
244
  # Stage 1: Add A + B
 
258
 
259
 
260
  def add_expr_paren(tensors: Dict[str, torch.Tensor]) -> None:
261
+ """Add expression circuit for (A + B) × C (parenthetical grouping)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  prefix = "arithmetic.expr_paren"
263
 
264
  # Stage 1: Add A + B
 
278
 
279
 
280
  def add_add3(tensors: Dict[str, torch.Tensor]) -> None:
281
+ """Add 3-operand 8-bit adder circuit."""
 
 
 
 
 
 
 
 
 
 
282
  # Stage 1: A + B -> temp
283
  for bit in range(8):
284
  add_full_adder(tensors, f"arithmetic.add3_8bit.stage1.fa{bit}")
 
289
 
290
 
291
  def add_shl_shr(tensors: Dict[str, torch.Tensor]) -> None:
292
+ """Add SHL (shift left) and SHR (shift right) circuits."""
 
 
 
 
 
 
 
293
  for bit in range(8):
294
  if bit < 7:
295
  add_gate(tensors, f"alu.alu8bit.shl.bit{bit}", [1.0], [-1.0])
 
304
 
305
 
306
  def add_mul(tensors: Dict[str, torch.Tensor]) -> None:
307
+ """Add 8-bit multiplication circuit."""
 
 
 
 
 
 
 
 
 
 
 
308
  # AND gates for partial products: P[i][j] = A[i] AND B[j]
309
  # These compute whether bit i of A and bit j of B are both 1
310
  for i in range(8):
 
313
 
314
 
315
  def add_div(tensors: Dict[str, torch.Tensor]) -> None:
316
+ """Add 8-bit division circuit."""
 
 
 
 
 
 
 
 
 
 
 
 
317
  # Comparison gates: check if (remainder << 1 | next_bit) >= divisor
318
  # Bit-cascaded ternary GE comparator per stage.
319
  for stage in range(8):
 
341
 
342
 
343
  def add_inc_dec(tensors: Dict[str, torch.Tensor]) -> None:
344
+ """Add INC and DEC circuits."""
 
 
 
 
 
 
 
345
  # INC: half adder chain starting with carry_in = 1
346
  # bit 7 (LSB): XOR with 1, carry = bit[7]
347
  # bit 6: XOR with carry, new_carry = bit[6] AND old_carry
 
368
 
369
 
370
  def add_neg(tensors: Dict[str, torch.Tensor]) -> None:
371
+ """Add NEG circuit (two's complement negation)."""
 
 
 
 
 
372
  for bit in range(8):
373
  # NOT gate for each bit
374
  add_gate(tensors, f"alu.alu8bit.neg.not.bit{bit}", [-1.0], [0.0])
 
385
 
386
 
387
  def add_rol_ror_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
388
+ """Add N-bit ROL and ROR circuits (rotate left/right)."""
 
 
 
 
 
 
 
389
  # ROL: rotate left (toward MSB)
390
  for bit in range(bits):
391
  add_gate(tensors, f"alu.alu{bits}bit.rol.bit{bit}", [1.0], [-1.0])
 
396
 
397
 
398
  def add_stack_ops(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
399
+ """Add RET, PUSH, POP circuit components."""
 
 
 
 
 
 
 
 
 
 
 
 
400
  # SP decrement for PUSH (addr_bits wide)
401
  for bit in range(addr_bits):
402
  add_gate(tensors, f"control.push.sp_dec.bit{bit}.xor.layer1.or", [1.0, 1.0], [-1.0])
 
425
 
426
 
427
  def add_conditional_jumps(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
428
+ """Add conditional jump circuits (JZ, JNZ, JC, JNC, JP, JN, JV, JNV)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  jump_types = ['jz', 'jnz', 'jc', 'jnc', 'jp', 'jn', 'jv', 'jnv']
430
 
431
  for jmp in jump_types:
 
442
 
443
 
444
  def add_computer_decode(tensors: Dict[str, torch.Tensor]) -> None:
445
+ """The 4-to-16 opcode one-hot decoder of the 8-bit computer."""
 
 
446
  for opc in range(16):
447
  w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(4)]
448
  add_gate(tensors, f"control.decode.op{opc}", w, [-float(bin(opc).count("1"))])
449
 
450
 
451
  def add_computer_pcnext(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
452
+ """Next-PC selection of the 8-bit computer among PC+2, PC+4, the jump mux and the direct target."""
 
 
 
453
  for name, start in (("pc2", 1), ("pc4", 2)):
454
  for bit in range(addr_bits):
455
  if bit < start:
 
469
 
470
 
471
  def add_status_flags(tensors: Dict[str, torch.Tensor], data_bits: int) -> None:
472
+ """Add status flag computation circuits (Z, N, C, V)."""
 
 
 
 
 
 
 
 
 
 
473
  # Z flag: NOR of all bits (result == 0)
474
  # Single threshold gate: fires if sum of all bits < 1
475
  add_gate(tensors, "flags.zero", [-1.0] * data_bits, [0.0])
 
493
 
494
 
495
  def add_barrel_shifter_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
496
+ """Add N-bit barrel shifter circuit."""
 
 
 
 
 
 
 
497
  import math
498
  num_layers = max(1, math.ceil(math.log2(bits)))
499
 
 
514
 
515
 
516
  def add_priority_encoder_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
517
+ """Add N-bit priority encoder circuit."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  import math
519
  out_bits = max(1, math.ceil(math.log2(bits)))
520
  prefix = f"combinational.priorityencoder{bits}"
 
558
  out_le: str,
559
  out_eq: str,
560
  ) -> None:
561
+ """Generic ternary-only N-bit comparator."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  for i in range(bits):
563
  # per-bit GT: A[i] AND NOT B[i] -> H(A - B - 1)
564
  add_gate(tensors, f"{cmp_prefix}.bit{i}.gt", [1.0, -1.0], [-1.0])
 
598
 
599
 
600
  def add_comparators(tensors: Dict[str, torch.Tensor]) -> None:
601
+ """Add 8-bit comparator circuits (GT, LT, GE, LE, EQ) using bit-cascade."""
 
 
 
 
602
  add_bit_cascade_compare(
603
  tensors,
604
  cmp_prefix="arithmetic.cmp8bit",
 
612
 
613
 
614
  def add_ripple_carry_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
615
+ """Add N-bit ripple carry adder circuit."""
 
 
 
 
 
 
 
616
  prefix = f"arithmetic.ripplecarry{bits}bit"
617
  for bit in range(bits):
618
  add_full_adder(tensors, f"{prefix}.fa{bit}")
619
 
620
 
621
  def add_sub_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
622
+ """Add N-bit subtractor circuit (A - B)."""
 
 
 
 
 
 
 
 
 
623
  prefix = f"arithmetic.sub{bits}bit"
624
 
625
  for bit in range(bits):
 
630
 
631
 
632
  def add_comparators_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
633
+ """Add N-bit comparator circuits (GT, LT, GE, LE, EQ) via bit-cascade."""
 
 
 
634
  add_bit_cascade_compare(
635
  tensors,
636
  cmp_prefix=f"arithmetic.cmp{bits}bit",
 
644
 
645
 
646
  def add_mul_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
647
+ """Add N-bit multiplication circuit."""
 
 
 
 
 
 
 
 
 
648
  for i in range(bits):
649
  for j in range(bits):
650
  add_gate(tensors, f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [1.0, 1.0], [-2.0])
651
 
652
 
653
  def add_div_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
654
+ """Add N-bit division circuit."""
 
 
 
 
655
  for stage in range(bits):
656
  add_bit_cascade_compare(
657
  tensors,
 
673
 
674
 
675
  def add_bitwise_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
676
+ """Add N-bit bitwise operation circuits (AND, OR, XOR, NOT)."""
 
 
 
677
  for bit in range(bits):
678
  add_gate(tensors, f"alu.alu{bits}bit.and.bit{bit}", [1.0, 1.0], [-2.0])
679
 
 
690
 
691
 
692
  def add_shift_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
693
+ """Add N-bit shift circuits (SHL, SHR by 1 position)."""
 
 
 
 
694
  for bit in range(bits):
695
  if bit < bits - 1:
696
  add_gate(tensors, f"alu.alu{bits}bit.shl.bit{bit}", [1.0], [-1.0])
 
731
 
732
 
733
  def add_float16_core(tensors: Dict[str, torch.Tensor]) -> None:
734
+ """Add float16 core circuits (unpack, pack, classify)."""
 
 
 
 
 
 
 
 
 
 
 
 
735
  prefix = "float16"
736
 
737
  # Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
 
755
 
756
  def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
757
  exp_bits: int, frac_bits: int) -> None:
758
+ """Add a complete float addition/subtraction pipeline."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  E, F = exp_bits, frac_bits
760
  P = E + F # payload width
761
  FRAME = F + 3 # mantissa + guard + round
 
972
 
973
  def add_float_mul(tensors: Dict[str, torch.Tensor], family: str,
974
  exp_bits: int, frac_bits: int) -> None:
975
+ """Add a complete float multiplication pipeline."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
  E, F = exp_bits, frac_bits
977
  prefix = f"{family}.mul"
978
 
 
1115
 
1116
  def add_float_div(tensors: Dict[str, torch.Tensor], family: str,
1117
  exp_bits: int, frac_bits: int) -> None:
1118
+ """Add a complete float division pipeline."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1119
  E, F = exp_bits, frac_bits
1120
  prefix = f"{family}.div"
1121
  NSTAGE = F + 3
 
1288
 
1289
  def add_float_fma(tensors: Dict[str, torch.Tensor], family: str,
1290
  exp_bits: int, frac_bits: int) -> None:
1291
+ """Fused multiply-add: round(a*b + c) with a SINGLE rounding (F-extension)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1292
  E, F = exp_bits, frac_bits
1293
  W = 1 + E + F
1294
  p = F + 1
 
1569
 
1570
  def add_float_cmp(tensors: Dict[str, torch.Tensor], family: str,
1571
  exp_bits: int, frac_bits: int) -> None:
1572
+ """Add a complete IEEE 754 comparison network (EQ, LT, LE, GT, GE)."""
 
 
 
 
 
 
 
 
 
 
 
1573
  prefix = f"{family}.cmp"
1574
  payload = exp_bits + frac_bits
1575
 
 
1657
 
1658
 
1659
  def add_float32_core(tensors: Dict[str, torch.Tensor]) -> None:
1660
+ """Add float32 core circuits (unpack, pack, classify)."""
 
 
 
 
 
 
1661
  prefix = "float32"
1662
 
1663
  # Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
 
1696
  add_float_div(tensors, "float32", 8, 23)
1697
 
1698
  def update_manifest(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int, mem_bytes: int) -> None:
1699
+ """Update manifest metadata tensors."""
 
 
 
 
 
 
1700
  tensors["manifest.data_bits"] = torch.tensor([float(data_bits)], dtype=torch.float32)
1701
  tensors["manifest.addr_bits"] = torch.tensor([float(addr_bits)], dtype=torch.float32)
1702
  tensors["manifest.memory_bytes"] = torch.tensor([float(mem_bytes)], dtype=torch.float32)
 
1793
 
1794
 
1795
  def infer_expr_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
1796
+ """Infer inputs for A + B × C expression circuit (order of operations)."""
 
 
 
 
 
 
 
 
 
1797
  prefix = "arithmetic.expr_add_mul"
1798
 
1799
  # Register all inputs
 
1904
 
1905
 
1906
  def infer_expr_paren_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
1907
+ """Infer inputs for (A + B) × C expression circuit (parenthetical override)."""
 
 
 
 
 
 
 
 
1908
  prefix = "arithmetic.expr_paren_add_mul"
1909
 
1910
  # Register all inputs
 
2011
 
2012
 
2013
  def infer_expr_paren_inputs(gate: str, reg: SignalRegistry) -> List[int]:
2014
+ """Infer inputs for (A + B) × C expression circuit (parenthetical grouping)."""
 
 
 
 
 
 
 
 
2015
  prefix = "arithmetic.expr_paren"
2016
 
2017
  # Register all inputs
 
2657
 
2658
  def _infer_bit_cascade(gate: str, reg: SignalRegistry, cmp_prefix: str,
2659
  a_template: str, b_template: str) -> List[int] | None:
2660
+ """If `gate` is part of the bit-cascade comparator at `cmp_prefix`, return its inputs."""
 
 
 
 
2661
  if not gate.startswith(cmp_prefix + "."):
2662
  return None
2663
  suffix = gate[len(cmp_prefix) + 1:]
 
2697
  def _infer_compare_final(gate: str, reg: SignalRegistry, cmp_prefix: str,
2698
  out_gt: str, out_lt: str, out_ge: str, out_le: str,
2699
  out_eq: str, bits: int) -> List[int] | None:
2700
+ """Inputs of the final outputs of a bit-cascade comparator."""
 
 
 
2701
  if gate == out_gt:
2702
  return ([reg.register(f"{cmp_prefix}.bit0.gt")]
2703
  + [reg.register(f"{cmp_prefix}.cascade.gt.bit{i}") for i in range(1, bits)])
 
2719
 
2720
  def infer_float_cmp_inputs(gate: str, reg: SignalRegistry, family: str,
2721
  exp_bits: int, frac_bits: int) -> Optional[List[int]]:
2722
+ """Complete wiring for the self-contained float comparison network."""
 
 
 
 
 
2723
  prefix = f"{family}.cmp"
2724
  if gate != prefix and not gate.startswith(prefix + "."):
2725
  return None
 
2834
 
2835
  def _fa_member_inputs(sub: str, fa_prefix: str, a_sig: int, b_sig: int,
2836
  cin_sig: int, reg: SignalRegistry) -> Optional[List[int]]:
2837
+ """Wiring for one gate of an add_full_adder cell, given the cell's operand/carry signal IDs."""
 
2838
  if sub == "ha1.sum.layer1.or" or sub == "ha1.sum.layer1.nand":
2839
  return [a_sig, b_sig]
2840
  if sub == "ha1.sum.layer2":
 
2857
 
2858
  def infer_float_mul_inputs(gate: str, reg: SignalRegistry, family: str,
2859
  exp_bits: int, frac_bits: int) -> Optional[List[int]]:
2860
+ """Complete wiring for the self-contained float multiplication pipeline."""
 
 
 
 
 
 
2861
  E, F = exp_bits, frac_bits
2862
  W = 1 + E + F
2863
  prefix = f"{family}.mul"
 
3134
 
3135
  def infer_float_div_inputs(gate: str, reg: SignalRegistry, family: str,
3136
  exp_bits: int, frac_bits: int) -> Optional[List[int]]:
3137
+ """Complete wiring for the self-contained float division pipeline."""
 
 
 
 
 
 
 
3138
  E, F = exp_bits, frac_bits
3139
  W = 1 + E + F
3140
  prefix = f"{family}.div"
 
4523
 
4524
 
4525
  def add_subleq_core(tensors: Dict[str, torch.Tensor]) -> None:
4526
+ """Add the SUBLEQ one-instruction datapath."""
 
 
 
 
 
 
 
 
4527
  for k in range(8):
4528
  add_gate(tensors, f"subleq.sub.not_a.bit{k}", [-1.0], [0.0])
4529
  add_full_adder(tensors, f"subleq.sub.fa{k}")
 
4591
 
4592
 
4593
  def infer_rv32_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
4594
+ """Wiring of the RV32 sub-circuits: MULH accumulator, SRA muxes, NEUR and hazard comparators."""
 
 
 
 
 
4595
  if not gate.startswith("rv32."):
4596
  return None
4597
 
 
4745
 
4746
 
4747
  def _infer_threshold_computer_bit_cascade(gate: str, reg: SignalRegistry) -> List[int] | None:
4748
+ """Match every bit-cascade comparator location in the codebase."""
 
 
 
4749
  # Integer arithmetic.cmp{N}bit.* + the public greaterthan/lessthan/etc.
4750
  m = re.match(r"^arithmetic\.cmp(\d+)bit\..+", gate)
4751
  if m:
 
4813
 
4814
 
4815
  def _load_routing_table() -> Dict[str, Any]:
4816
+ """Load `routing/routing.json` once; on missing file, return {}."""
 
 
 
 
 
 
 
 
 
4817
  global _ROUTING_TABLE_CACHE
4818
  if _ROUTING_TABLE_CACHE is not None:
4819
  return _ROUTING_TABLE_CACHE
 
4831
  gate: str, reg: SignalRegistry,
4832
  known_gate_suffixes: Optional[Set[str]] = None,
4833
  ) -> Optional[List[int]]:
4834
+ """Resolve a gate's inputs by consulting routing.json."""
 
 
 
 
 
 
 
 
 
4835
  table = _load_routing_table()
4836
  if not table:
4837
  return None
 
4931
  return fc
4932
  # Bit-cascade comparators (integer + float + div) and ternary modular detectors.
4933
  # These are the gates emitted by add_bit_cascade_compare and the ternary
4934
+ # modular rebuild in the quantize command; cover them up front so they don't fall
4935
  # through to the generic per-family handlers below.
4936
  bc = _infer_threshold_computer_bit_cascade(gate, reg)
4937
  if bc is not None:
 
5601
 
5602
 
5603
  def cmd_subleq(args) -> None:
5604
+ """Build the standalone SUBLEQ-8 machine file: 256 bytes of packed memory and the datapath."""
 
5605
  out = args.model if args.model else MODEL_DIR / "variants" / "neural_subleq8.safetensors"
5606
  print("=" * 60)
5607
  print(" BUILD SUBLEQ-8 (one-instruction threshold machine)")
 
5632
 
5633
 
5634
  def add_rv32_extras(tensors: Dict[str, torch.Tensor]) -> None:
5635
+ """RV32 circuits beyond the shared families: MULH accumulator, SRA muxes, NEUR, hazard comparators."""
 
 
 
5636
  # 64-bit shift-add accumulator over the 32x32 partial products.
5637
  for stage in range(31):
5638
  for bit in range(64):
 
5671
  RV32_OPCLASS = {0x33: "op", 0x13: "op_imm", 0x03: "load", 0x23: "store",
5672
  0x63: "branch", 0x37: "lui", 0x17: "auipc", 0x6F: "jal",
5673
  0x67: "jalr", 0x73: "system", 0x53: "fp", 0x07: "flw", 0x27: "fsw"}
5674
+
5675
  # Which opcode classes select each immediate format.
5676
  RV32_USE = {"I": ["op_imm", "load", "jalr", "flw"], "S": ["store", "fsw"],
5677
  "B": ["branch"], "U": ["lui", "auipc"], "J": ["jal"]}
5678
+
5679
  RV32_IMM_FMTS = ("I", "S", "B", "U", "J")
5680
 
5681
 
 
5713
 
5714
 
5715
  def add_rv32_decode(tensors: Dict[str, torch.Tensor]) -> None:
5716
+ """RV32 instruction decode: opcode-class one-hots and the sign-extended immediate."""
 
 
 
5717
  for opc, name in RV32_OPCLASS.items():
5718
  w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(7)]
5719
  add_gate(tensors, f"rv32.decode.is_{name}", w, [-float(bin(opc).count("1"))])
 
5729
 
5730
 
5731
  def add_rv32_pcnext(tensors: Dict[str, torch.Tensor]) -> None:
5732
+ """RV32 next-PC selection among PC+4, PC+imm and (rs1+imm)&~1."""
 
 
 
5733
  for k in range(32):
5734
  # m1 = sel_target ? pcimm : pc4
5735
  add_gate(tensors, f"rv32.pcnext.m1.bit{k}.not_sel", [-1.0], [0.0])
 
5744
 
5745
 
5746
  def cmd_rv32(args) -> None:
5747
+ """Build the standalone RV32 processor file: 64 KB of memory, the shared families and the FPU."""
 
 
5748
  out = args.model if args.model else MODEL_DIR / "variants" / "neural_rv32.safetensors"
5749
  print("=" * 60)
5750
  print(" BUILD NEURAL_RV32 (RV32IM + F-subset threshold processor)")
 
5928
  else:
5929
  parser.print_help()
5930
 
5931
+ DTYPES = [
5932
+ (torch.int8, -(1 << 7), (1 << 7) - 1),
5933
+ (torch.int16, -(1 << 15), (1 << 15) - 1),
5934
+ (torch.int32, -(1 << 31), (1 << 31) - 1),
5935
+ (torch.int64, None, None), # always fits
5936
+ ]
5937
+
5938
+
5939
+ def _normalize_to_int(tensor: torch.Tensor) -> torch.Tensor:
5940
+ """Return a tensor with strictly integer values, floored from any half-integer values."""
5941
+ if not tensor.dtype.is_floating_point:
5942
+ return tensor.to(torch.float64) # promote for range checks
5943
+ tf = tensor.to(torch.float64)
5944
+ rounded = tf.round()
5945
+ if torch.equal(rounded, tf):
5946
+ return tf
5947
+ doubled = tf * 2.0
5948
+ if torch.equal(doubled.round(), doubled):
5949
+ return torch.floor(tf)
5950
+ raise ValueError(
5951
+ f"tensor has non-half-integer values; range "
5952
+ f"[{tf.min().item()}, {tf.max().item()}]"
5953
+ )
5954
+
5955
+
5956
+ def _min_signed_int_dtype(tensor: torch.Tensor) -> torch.dtype:
5957
+ if tensor.numel() == 0:
5958
+ return torch.int8
5959
+ lo = int(tensor.min().item())
5960
+ hi = int(tensor.max().item())
5961
+ for dtype, lo_lim, hi_lim in DTYPES:
5962
+ if lo_lim is None or (lo_lim <= lo and hi <= hi_lim):
5963
+ return dtype
5964
+ return torch.int64
5965
+
5966
+
5967
+ def _ternarize_modular_and_patterns(
5968
+ tensors: Dict[str, torch.Tensor],
5969
+ ) -> Tuple[Dict[str, torch.Tensor], Dict]:
5970
+ """Replace the seed file's modular detectors with bit-cascade equivalents."""
5971
+ new_tensors = dict(tensors)
5972
+
5973
+ # --- pattern_recognition: drop leadingones/trailingones ---
5974
+ pr_dropped = 0
5975
+ for k in list(new_tensors.keys()):
5976
+ if (k.startswith("pattern_recognition.leadingones")
5977
+ or k.startswith("pattern_recognition.trailingones")):
5978
+ del new_tensors[k]
5979
+ pr_dropped += 1
5980
+
5981
+ # --- modular: rebuild as ternary bit-cascade equality per multiple ---
5982
+ moduli = [3, 5, 6, 7, 9, 10, 11, 12]
5983
+ mod_gates_added = 0
5984
+ for mod in moduli:
5985
+ prefix = f"modular.mod{mod}"
5986
+ if f"{prefix}.eq.k0.all.weight" in new_tensors:
5987
+ continue # already bit-cascade; rebuilding would only shed metadata
5988
+ # Drop old structure
5989
+ for k in list(new_tensors.keys()):
5990
+ if k.startswith(prefix + "."):
5991
+ del new_tensors[k]
5992
+
5993
+ multiples = list(range(0, 256, mod))
5994
+ # Per-bit match gates + per-multiple AND
5995
+ for k in multiples:
5996
+ for i in range(8): # i=0 is MSB (matches inputs MSB-first ordering)
5997
+ k_bit = (k >> (7 - i)) & 1
5998
+ if k_bit == 1:
5999
+ # bit_match = x[i]: H(x - 0.5) ~~ identity for binary;
6000
+ # use weight=1, bias=-1 -> H(x-1): x=0 -> 0, x=1 -> 1.
6001
+ new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.weight"] = torch.tensor([1.0], dtype=torch.float64)
6002
+ new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.bias"] = torch.tensor([-1.0], dtype=torch.float64)
6003
+ else:
6004
+ # bit_match = NOT x[i]: weight=-1, bias=0 -> H(-x): x=0 -> 1, x=1 -> 0.
6005
+ new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.weight"] = torch.tensor([-1.0], dtype=torch.float64)
6006
+ new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.bias"] = torch.tensor([0.0], dtype=torch.float64)
6007
+ mod_gates_added += 1
6008
+ # AND of all 8 bit-matches: weights [1]*8, bias -8
6009
+ new_tensors[f"{prefix}.eq.k{k}.all.weight"] = torch.tensor([1.0] * 8, dtype=torch.float64)
6010
+ new_tensors[f"{prefix}.eq.k{k}.all.bias"] = torch.tensor([-8.0], dtype=torch.float64)
6011
+ mod_gates_added += 1
6012
+
6013
+ # Final OR over all per-multiple match outputs
6014
+ m = len(multiples)
6015
+ new_tensors[f"{prefix}.weight"] = torch.tensor([1.0] * m, dtype=torch.float64)
6016
+ new_tensors[f"{prefix}.bias"] = torch.tensor([-1.0], dtype=torch.float64)
6017
+ mod_gates_added += 1
6018
+
6019
+ return new_tensors, {
6020
+ "pattern_recognition_dropped": pr_dropped,
6021
+ "modular_gates_added": mod_gates_added,
6022
+ "modular_moduli": len(moduli),
6023
+ }
6024
+
6025
+
6026
+ def _ternarize_buffers(
6027
+ tensors: Dict[str, torch.Tensor],
6028
+ ) -> Tuple[Dict[str, torch.Tensor], Dict]:
6029
+ """Rewrite weight +-2 identity buffers as weight +-1 with the bias adjusted."""
6030
+ new_tensors = dict(tensors)
6031
+ fixed = 0
6032
+ failed_names = []
6033
+
6034
+ weight_keys = [k for k in tensors if k.endswith(".weight")]
6035
+ for wkey in weight_keys:
6036
+ w = tensors[wkey]
6037
+ wf = w.float() if w.dtype.is_floating_point else w.to(torch.float64).float()
6038
+ if (wf.abs() <= 1.0).all():
6039
+ continue # already ternary
6040
+
6041
+ gate = wkey[: -len(".weight")]
6042
+ bkey = gate + ".bias"
6043
+
6044
+ # Single-input weight=+-2 buffer with single bias
6045
+ if (
6046
+ wf.numel() == 1
6047
+ and abs(wf.item()) == 2.0
6048
+ and bkey in tensors
6049
+ and tensors[bkey].numel() == 1
6050
+ ):
6051
+ w_val = wf.item()
6052
+ b_val = float(tensors[bkey].float().item())
6053
+ sgn = 1.0 if w_val > 0 else -1.0
6054
+ x0_target = 1 if b_val >= 0 else 0
6055
+ x1_target = 1 if (w_val + b_val) >= 0 else 0
6056
+ chosen = None
6057
+ # Prefer keeping the bias unchanged when possible
6058
+ for b_new in [int(b_val), int(b_val) - 1, -1, 0, -2, 1, -3, 2]:
6059
+ x0 = 1 if b_new >= 0 else 0
6060
+ x1 = 1 if (sgn + b_new) >= 0 else 0
6061
+ if x0 == x0_target and x1 == x1_target:
6062
+ chosen = b_new
6063
+ break
6064
+ if chosen is not None:
6065
+ new_tensors[wkey] = torch.tensor([sgn], dtype=torch.float64)
6066
+ new_tensors[bkey] = torch.tensor([float(chosen)], dtype=torch.float64)
6067
+ fixed += 1
6068
+ continue
6069
+
6070
+ failed_names.append(wkey)
6071
+
6072
+ return new_tensors, {"fixed": fixed, "failed_names": failed_names}
6073
+
6074
+
6075
+ def quantize_tensors(
6076
+ tensors: Dict[str, torch.Tensor],
6077
+ ternary: bool = False,
6078
+ ) -> Tuple[Dict[str, torch.Tensor], Dict[str, int], Tuple[int, int], Dict]:
6079
+ """Quantize a dict of tensors."""
6080
+ ternary_stats: Dict = {"applied": False, "fixed": 0, "failed_names": []}
6081
+ if ternary:
6082
+ tensors, ternary_stats = _ternarize_buffers(tensors)
6083
+ ternary_stats["applied"] = True
6084
+ # Also rebuild modular detectors and drop pattern_recognition stragglers.
6085
+ tensors, mod_stats = _ternarize_modular_and_patterns(tensors)
6086
+ ternary_stats["modular_gates_added"] = mod_stats["modular_gates_added"]
6087
+ ternary_stats["pattern_recognition_dropped"] = mod_stats["pattern_recognition_dropped"]
6088
+
6089
+ new_tensors: Dict[str, torch.Tensor] = {}
6090
+ counts: Dict[str, int] = {"int8": 0, "int16": 0, "int32": 0, "int64": 0,
6091
+ "manifest_kept": 0, "skipped": 0}
6092
+ bytes_before = 0
6093
+ bytes_after = 0
6094
+
6095
+ for name, t in tensors.items():
6096
+ bytes_before += t.numel() * t.element_size()
6097
+
6098
+ if name.startswith("manifest."):
6099
+ new_tensors[name] = t
6100
+ counts["manifest_kept"] += 1
6101
+ bytes_after += t.numel() * t.element_size()
6102
+ continue
6103
+
6104
+ try:
6105
+ normalized = _normalize_to_int(t)
6106
+ except ValueError:
6107
+ new_tensors[name] = t
6108
+ counts["skipped"] += 1
6109
+ bytes_after += t.numel() * t.element_size()
6110
+ continue
6111
+
6112
+ target = _min_signed_int_dtype(normalized)
6113
+ cast = normalized.to(target)
6114
+ new_tensors[name] = cast
6115
+ bytes_after += cast.numel() * cast.element_size()
6116
+ counts[str(target).replace("torch.", "")] += 1
6117
+
6118
+ return new_tensors, counts, (bytes_before, bytes_after), ternary_stats
6119
+
6120
+
6121
+ def quantize_file(in_path: Path, out_path: Path, verbose: bool = False,
6122
+ ternary: bool = False, strict_ternary: bool = False) -> Dict:
6123
+ file_before = in_path.stat().st_size
6124
+ tensors: Dict[str, torch.Tensor] = {}
6125
+ metadata: Dict[str, str] = {}
6126
+ with safe_open(str(in_path), framework="pt") as f:
6127
+ meta = f.metadata()
6128
+ if meta:
6129
+ metadata = dict(meta)
6130
+ for name in f.keys():
6131
+ # clone so the source mmap can be released before we write
6132
+ tensors[name] = f.get_tensor(name).clone()
6133
+
6134
+ new_tensors, counts, (before, after), tstats = quantize_tensors(tensors, ternary=ternary)
6135
+
6136
+ # Audit final ternary status (count of weight tensors with |w| > 1)
6137
+ final_nonternary = []
6138
+ for k, v in new_tensors.items():
6139
+ if not k.endswith(".weight"):
6140
+ continue
6141
+ if k.startswith("manifest."):
6142
+ continue
6143
+ vf = v.float() if v.dtype.is_floating_point else v.to(torch.float64).float()
6144
+ if (vf.abs() > 1.0).any():
6145
+ final_nonternary.append(k)
6146
+
6147
+ if ternary and strict_ternary and final_nonternary:
6148
+ raise ValueError(
6149
+ f"--strict failed: {len(final_nonternary)} weight tensors are not "
6150
+ f"ternary after transformation; first: {final_nonternary[:5]}"
6151
+ )
6152
+
6153
+ # Drop the original mmap-backed tensors before writing in-place.
6154
+ del tensors
6155
+ out_path.parent.mkdir(parents=True, exist_ok=True)
6156
+ if ternary:
6157
+ # Note ternary mode in metadata so downstream tools can see it
6158
+ if metadata is None:
6159
+ metadata = {}
6160
+ metadata = dict(metadata)
6161
+ metadata["weight_quantization"] = (
6162
+ "ternary_partial" if final_nonternary else "ternary"
6163
+ )
6164
+ save_file(new_tensors, str(out_path), metadata=metadata or None)
6165
+
6166
+ file_after = out_path.stat().st_size
6167
+ return {
6168
+ "in_path": str(in_path),
6169
+ "out_path": str(out_path),
6170
+ "tensor_counts": counts,
6171
+ "tensor_bytes_before": before,
6172
+ "tensor_bytes_after": after,
6173
+ "file_size_before": file_before,
6174
+ "file_size_after": file_after,
6175
+ "ternary": tstats,
6176
+ "final_nonternary": final_nonternary,
6177
+ }
6178
+
6179
+
6180
+ def _print_summary(label: str, info: Dict) -> None:
6181
+ cb = info["tensor_bytes_before"]
6182
+ ca = info["tensor_bytes_after"]
6183
+ fb = info["file_size_before"]
6184
+ fa = info["file_size_after"]
6185
+ counts = info["tensor_counts"]
6186
+ bucket_str = " ".join(f"{k}={v}" for k, v in counts.items() if v)
6187
+ ratio_t = cb / ca if ca else 1.0
6188
+ ratio_f = fb / fa if fa else 1.0
6189
+ print(
6190
+ f" {label}: file {fb / 1e6:6.1f} MB -> {fa / 1e6:6.1f} MB "
6191
+ f"({ratio_f:.2f}x); tensor data {cb / 1e6:6.1f} MB -> {ca / 1e6:6.1f} MB "
6192
+ f"({ratio_t:.2f}x)"
6193
+ )
6194
+ print(f" {bucket_str}")
6195
+ if info.get("ternary", {}).get("applied"):
6196
+ ts = info["ternary"]
6197
+ nt = info["final_nonternary"]
6198
+ print(f" ternary: {ts['fixed']} buffer gates rewritten; "
6199
+ f"{len(nt)} weight tensors remain non-ternary")
6200
+
6201
+
6202
+ def main_quantize() -> int:
6203
+ parser = argparse.ArgumentParser(description="Quantize safetensors to min signed int dtype")
6204
+ parser.add_argument("input", type=Path, help=".safetensors file or directory of files")
6205
+ parser.add_argument("-o", "--output", type=Path, default=None,
6206
+ help="output file or directory (default: in-place)")
6207
+ parser.add_argument("-v", "--verbose", action="store_true")
6208
+ parser.add_argument("--ternary", action="store_true",
6209
+ help="Rewrite single-input weight=+/-2 buffers as +/-1 to push toward "
6210
+ "ternary {-1, 0, 1} weights and report remaining violations")
6211
+ parser.add_argument("--strict", action="store_true",
6212
+ help="With --ternary, fail if any weight tensor is still non-ternary")
6213
+ parser.add_argument("--report-violations", type=int, default=0, metavar="N",
6214
+ help="Print first N non-ternary weight tensor names per file")
6215
+ args = parser.parse_args()
6216
+
6217
+ inputs = []
6218
+ if args.input.is_dir():
6219
+ inputs = sorted(p for p in args.input.glob("*.safetensors"))
6220
+ elif args.input.is_file():
6221
+ inputs = [args.input]
6222
+ else:
6223
+ print(f"not found: {args.input}", file=sys.stderr)
6224
+ return 2
6225
+
6226
+ if not inputs:
6227
+ print(f"no .safetensors files under {args.input}", file=sys.stderr)
6228
+ return 2
6229
+
6230
+ if args.output is None:
6231
+ outputs = inputs # in-place
6232
+ elif args.output.suffix == ".safetensors":
6233
+ if len(inputs) != 1:
6234
+ print("output is a single file but input is a directory; pass a directory output", file=sys.stderr)
6235
+ return 2
6236
+ outputs = [args.output]
6237
+ else:
6238
+ args.output.mkdir(parents=True, exist_ok=True)
6239
+ outputs = [args.output / p.name for p in inputs]
6240
+
6241
+ total_before = 0
6242
+ total_after = 0
6243
+ print(f"Quantizing {len(inputs)} file(s)" + (" (ternary mode)" if args.ternary else "") + "\n")
6244
+ for src, dst in zip(inputs, outputs):
6245
+ info = quantize_file(src, dst, verbose=args.verbose,
6246
+ ternary=args.ternary, strict_ternary=args.strict)
6247
+ _print_summary(src.name, info)
6248
+ if args.report_violations and info.get("final_nonternary"):
6249
+ for name in info["final_nonternary"][: args.report_violations]:
6250
+ print(f" non-ternary: {name}")
6251
+ if len(info["final_nonternary"]) > args.report_violations:
6252
+ print(f" ... and {len(info['final_nonternary']) - args.report_violations} more")
6253
+ total_before += info["file_size_before"]
6254
+ total_after += info["file_size_after"]
6255
+
6256
+ print()
6257
+ print("=" * 76)
6258
+ print(
6259
+ f"Total: {total_before / 1e6:.1f} MB -> {total_after / 1e6:.1f} MB "
6260
+ f"({total_before / max(total_after, 1):.2f}x reduction)"
6261
+ )
6262
+ return 0
6263
+
6264
+ ROOT = Path(__file__).resolve().parent.parent
6265
+
6266
+ SEED = ROOT / "neural_computer.safetensors"
6267
+
6268
+ OUT_DIR = ROOT / "variants"
6269
+
6270
+ PROFILES = ["none", "registers", "scratchpad", "small", "reduced", "full"]
6271
+
6272
+ BITS = [8, 16, 32]
6273
+
6274
+
6275
+ def variant_filename(bits: int, profile: str) -> str:
6276
+ if profile == "none":
6277
+ return f"neural_alu{bits}.safetensors"
6278
+ if profile == "full":
6279
+ return f"neural_computer{bits}.safetensors"
6280
+ return f"neural_computer{bits}_{profile}.safetensors"
6281
+
6282
+
6283
+ def run(cmd: list[str], timeout: int = 600) -> tuple[int, str]:
6284
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
6285
+ return p.returncode, (p.stdout or "") + (p.stderr or "")
6286
+
6287
+
6288
+ def build_variant(bits: int, profile: str) -> Path:
6289
+ out = OUT_DIR / variant_filename(bits, profile)
6290
+ shutil.copy2(SEED, out)
6291
+ cmd = [
6292
+ sys.executable, str(ROOT / "src" / "build.py"),
6293
+ "--bits", str(bits),
6294
+ "-m", profile,
6295
+ "--apply",
6296
+ "--model", str(out),
6297
+ "all",
6298
+ ]
6299
+ rc, log = run(cmd, timeout=2400)
6300
+ if rc != 0:
6301
+ raise RuntimeError(f"build failed for bits={bits} profile={profile}:\n{log[-1500:]}")
6302
+ return out
6303
+
6304
+
6305
+ def quantize_variant(path: Path) -> tuple[int, int]:
6306
+ """Run the quantize command with --ternary --strict on a built variant."""
6307
+ rc, log = run([sys.executable, str(ROOT / "src" / "build.py"), "quantize", str(path),
6308
+ "--ternary", "--strict"], timeout=300)
6309
+ if rc != 0:
6310
+ raise RuntimeError(f"quantize failed for {path.name}:\n{log[-800:]}")
6311
+ # parse the "file X.X MB -> Y.Y MB" line
6312
+ for line in log.splitlines():
6313
+ if "file" in line and "->" in line and path.name in line:
6314
+ try:
6315
+ parts = line.split("file")[1].split("->")
6316
+ before = float(parts[0].strip().split()[0]) * 1e6
6317
+ after = float(parts[1].strip().split()[0]) * 1e6
6318
+ return int(before), int(after)
6319
+ except Exception:
6320
+ pass
6321
+ return 0, 0
6322
+
6323
+
6324
+ def measure_variant(path: Path) -> dict:
6325
+ """Read tensor count, params, manifest values from the variant."""
6326
+ with safe_open(str(path), framework="pt") as f:
6327
+ keys = list(f.keys())
6328
+ params = sum(f.get_tensor(k).numel() for k in keys)
6329
+ manifest = {
6330
+ k.split(".", 1)[1]: f.get_tensor(k).item()
6331
+ for k in keys if k.startswith("manifest.") and f.get_tensor(k).numel() == 1
6332
+ }
6333
+ return {
6334
+ "tensors": len(keys),
6335
+ "params": params,
6336
+ "size_mb": path.stat().st_size / (1024 * 1024),
6337
+ "manifest": manifest,
6338
+ }
6339
+
6340
+
6341
+ def eval_variant(path: Path, device: str = "cpu", timeout: int = 600) -> dict:
6342
+ """Run eval.py against a variant and parse fitness."""
6343
+ cmd = [
6344
+ sys.executable, str(ROOT / "src" / "eval.py"),
6345
+ "--model", str(path),
6346
+ "--device", device,
6347
+ "--quiet",
6348
+ ]
6349
+ t0 = time.time()
6350
+ rc, log = run(cmd, timeout=timeout)
6351
+ dt = time.time() - t0
6352
+
6353
+ fitness = None
6354
+ total_tests = None
6355
+ status = "ERROR"
6356
+ for line in log.splitlines():
6357
+ line = line.strip()
6358
+ if line.startswith("Fitness:"):
6359
+ try:
6360
+ fitness = float(line.split()[1])
6361
+ except Exception:
6362
+ pass
6363
+ elif line.startswith("Total tests:"):
6364
+ try:
6365
+ total_tests = int(line.split()[2])
6366
+ except Exception:
6367
+ pass
6368
+ elif line.startswith("STATUS:"):
6369
+ status = line.split()[1]
6370
+ return {
6371
+ "rc": rc,
6372
+ "fitness": fitness,
6373
+ "total_tests": total_tests,
6374
+ "status": status,
6375
+ "elapsed_s": dt,
6376
+ "log_tail": "\n".join(log.splitlines()[-15:]),
6377
+ }
6378
+
6379
+
6380
+ def main_all() -> None:
6381
+ OUT_DIR.mkdir(exist_ok=True)
6382
+ rows = []
6383
+ print(f"Building 18 variants into {OUT_DIR}\n")
6384
+ for bits in BITS:
6385
+ for profile in PROFILES:
6386
+ label = f"bits={bits} profile={profile}"
6387
+ print(f"=== {label} ===", flush=True)
6388
+ t0 = time.time()
6389
+ try:
6390
+ path = build_variant(bits, profile)
6391
+ bt = time.time() - t0
6392
+ pre_q_meta = measure_variant(path)
6393
+ # Quantize in-place as the final step; weights are
6394
+ # integer-valued so this is exact, --strict fails the build
6395
+ # if any weight is non-ternary, and header metadata
6396
+ # (signal_registry) is carried through.
6397
+ qb, qa = quantize_variant(path)
6398
+ meta = measure_variant(path)
6399
+ ev = eval_variant(path, device="cpu", timeout=900)
6400
+ rows.append({
6401
+ "bits": bits, "profile": profile,
6402
+ "filename": path.name,
6403
+ "build_s": bt,
6404
+ **meta,
6405
+ **{k: ev[k] for k in ("fitness", "total_tests", "status", "elapsed_s")},
6406
+ "log_tail": ev["log_tail"] if ev["status"] != "PASS" else "",
6407
+ })
6408
+ q_ratio = qb / qa if qa else 1.0
6409
+ print(f" built in {bt:.1f}s size={pre_q_meta['size_mb']:.1f}MB -> "
6410
+ f"{meta['size_mb']:.1f}MB after quant ({q_ratio:.2f}x)"
6411
+ f" params={meta['params']:,} tensors={meta['tensors']:,}")
6412
+ print(f" eval: fitness={ev['fitness']} tests={ev['total_tests']}"
6413
+ f" status={ev['status']} ({ev['elapsed_s']:.1f}s)")
6414
+ if ev["status"] != "PASS":
6415
+ print(" --- failure tail ---")
6416
+ print(" " + "\n ".join(ev["log_tail"].splitlines()))
6417
+ print(" --------------------")
6418
+ except Exception as e:
6419
+ print(f" EXCEPTION: {e}")
6420
+ rows.append({"bits": bits, "profile": profile, "error": str(e)})
6421
+ print()
6422
+
6423
+ print("=" * 88)
6424
+ print(" SUMMARY")
6425
+ print("=" * 88)
6426
+ header = f"{'bits':>4} {'profile':<11} {'size_MB':>8} {'tensors':>8} {'params':>11} {'fitness':>9} {'tests':>6} {'status':>7}"
6427
+ print(header)
6428
+ print("-" * len(header))
6429
+ for r in rows:
6430
+ if "error" in r:
6431
+ print(f"{r['bits']:>4} {r['profile']:<11} ERROR: {r['error'][:60]}")
6432
+ continue
6433
+ fit = f"{r['fitness']:.4f}" if r['fitness'] is not None else "n/a"
6434
+ tests = r['total_tests'] if r['total_tests'] is not None else "?"
6435
+ print(f"{r['bits']:>4} {r['profile']:<11} {r['size_mb']:>8.1f} "
6436
+ f"{r['tensors']:>8,} {r['params']:>11,} "
6437
+ f"{fit:>9} {tests:>6} {r['status']:>7}")
6438
+
6439
+ fail = [r for r in rows if r.get("status") != "PASS" or "error" in r]
6440
+ print()
6441
+ if fail:
6442
+ print(f"FAILURES: {len(fail)}/{len(rows)}")
6443
+ else:
6444
+ print(f"ALL {len(rows)} VARIANTS PASS")
6445
+
6446
+
6447
+ COMMANDS = {'build': main, 'quantize': main_quantize, 'variants': main_all}
6448
+
6449
 
6450
  if __name__ == "__main__":
6451
+ if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
6452
+ sys.exit(COMMANDS['build']())
6453
+ cmd = sys.argv.pop(1)
6454
+ sys.argv[0] += ' ' + cmd
6455
+ sys.exit(COMMANDS[cmd]())
src/ca.py DELETED
@@ -1,261 +0,0 @@
1
- """Reversible Margolus (partitioned) cellular automaton.
2
-
3
- One fixed rule is applied to every 2x2 block, alternating the block partition
4
- between even and odd alignment each step (the Margolus neighborhood). The state
5
- is the lattice; there is no program counter, register file, or control circuit.
6
-
7
- Rule (cells ordered TL,TR,BL,BR): rotate the block 180 degrees, except a pair of
8
- particles on a diagonal (1001, 0110) swaps to the other diagonal. Both cases are
9
- involutions and neither maps a state across the diagonal-pair boundary, so the
10
- rule is a self-inverse permutation of the sixteen block states; the lattice
11
- update is therefore a bijection and replaying the partition sequence in reverse
12
- inverts the evolution.
13
-
14
- Dynamics are the billiard-ball model's: isolated particles move ballistically on
15
- diagonals and collisions deflect them reversibly. and_gate() computes AND from a
16
- collision; ballistic transport plus this collision are the primitives of the
17
- Fredkin-Toffoli universality construction (Margolus 1984).
18
- """
19
- from __future__ import annotations
20
- from typing import List, Tuple
21
-
22
- Block = Tuple[int, int, int, int]
23
-
24
-
25
- def rule(b: Block) -> Block:
26
- tl, tr, bl, br = b
27
- if b == (1, 0, 0, 1):
28
- return (0, 1, 1, 0) # diagonal pair -> other diagonal (deflect)
29
- if b == (0, 1, 1, 0):
30
- return (1, 0, 0, 1)
31
- return (br, bl, tr, tl) # otherwise rotate 180 degrees
32
-
33
-
34
- def is_bijection() -> bool:
35
- outs = {rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)}
36
- return len(outs) == 16
37
-
38
-
39
- def self_inverse() -> bool:
40
- return all(rule(rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))))
41
- == tuple((s >> k) & 1 for k in (3, 2, 1, 0)) for s in range(16))
42
-
43
-
44
- def step(grid: List[List[int]], phase: int) -> List[List[int]]:
45
- """One Margolus update. phase 0 aligns blocks at even coordinates; phase 1
46
- offsets the partition by (1,1). Toroidal, so H and W must be even."""
47
- H, W = len(grid), len(grid[0])
48
- out = [row[:] for row in grid]
49
- o = phase
50
- for r0 in range(o, o + H, 2):
51
- for c0 in range(o, o + W, 2):
52
- r, r1 = r0 % H, (r0 + 1) % H
53
- c, c1 = c0 % W, (c0 + 1) % W
54
- nb = rule((grid[r][c], grid[r][c1], grid[r1][c], grid[r1][c1]))
55
- out[r][c], out[r][c1], out[r1][c], out[r1][c1] = nb
56
- return out
57
-
58
-
59
- def run(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
60
- g = grid
61
- for n in range(nsteps):
62
- g = step(g, (start_phase + n) & 1)
63
- return g
64
-
65
-
66
- def run_back(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
67
- """Undo `run`: replay the phase sequence in reverse; the rule is self-inverse."""
68
- phases = [(start_phase + n) & 1 for n in range(nsteps)]
69
- g = grid
70
- for p in reversed(phases):
71
- g = step(g, p)
72
- return g
73
-
74
-
75
- # --- the block rule as Heaviside threshold gates ---
76
- # rule(s) = rotate180(s) XOR is_diag(s) on every cell: rotation fixes diagonal
77
- # pairs, and flipping all four cells of a rotated diagonal pair sends it to the
78
- # other diagonal. is_diag detects the two diagonal-pair states.
79
- def _H(x):
80
- return 1 if x >= 0 else 0
81
-
82
-
83
- def _and(*xs):
84
- return _H(sum(xs) - len(xs))
85
-
86
-
87
- def _or(*xs):
88
- return _H(sum(xs) - 1)
89
-
90
-
91
- def _xor(a, b):
92
- return _and(_or(a, b), _H(1 - a - b)) # OR AND NAND, the family's XOR
93
-
94
-
95
- def gate_rule(b: Block) -> Block:
96
- tl, tr, bl, br = b
97
- d = _or(_and(tl, 1 - tr, 1 - bl, br), _and(1 - tl, tr, bl, 1 - br)) # is_diag
98
- return (_xor(br, d), _xor(bl, d), _xor(tr, d), _xor(tl, d))
99
-
100
-
101
- def _test_gates():
102
- ok = all(gate_rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0)))
103
- == rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16))
104
- print(f" block rule as Heaviside threshold gates matches over 16 states: "
105
- f"{'OK' if ok else 'FAIL'}")
106
- return ok
107
-
108
-
109
- # --- tests ---
110
- def _rand_grid(H, W, seed):
111
- import random
112
- rng = random.Random(seed)
113
- return [[rng.randint(0, 1) for _ in range(W)] for _ in range(H)]
114
-
115
-
116
- def _ball_positions(g):
117
- return {(r, c) for r, row in enumerate(g) for c, v in enumerate(row) if v}
118
-
119
-
120
- def _test_rule():
121
- print(f" block rule is a bijection of 16 states: {'OK' if is_bijection() else 'FAIL'}")
122
- print(f" block rule is self-inverse: {'OK' if self_inverse() else 'FAIL'}")
123
- return is_bijection() and self_inverse()
124
-
125
-
126
- def _test_reversibility():
127
- bad = 0
128
- for seed in range(20):
129
- g = _rand_grid(8, 8, seed)
130
- fwd = run(g, 25, start_phase=0)
131
- back = run_back(fwd, 25, start_phase=0)
132
- if back != g:
133
- bad += 1
134
- # particle count is conserved (the rule permutes cells within each block)
135
- g = _rand_grid(8, 8, 99)
136
- conserved = sum(sum(r) for r in g) == sum(sum(r) for r in run(g, 40))
137
- print(f" lattice reversible (run then reverse recovers grid, 20 grids): "
138
- f"{'OK' if bad == 0 else f'FAIL({bad})'}")
139
- print(f" particle number conserved: {'OK' if conserved else 'FAIL'}")
140
- return bad == 0 and conserved
141
-
142
-
143
- def _test_ballistic():
144
- # a single particle travels in a straight diagonal line
145
- H = W = 16
146
- g = [[0] * W for _ in range(H)]
147
- g[2][2] = 1
148
- positions = [next(iter(_ball_positions(g)))]
149
- gg = g
150
- for n in range(8):
151
- gg = step(gg, n & 1)
152
- p = _ball_positions(gg)
153
- positions.append(next(iter(p)) if len(p) == 1 else None)
154
- ok = all(p is not None for p in positions)
155
- steady = ok and all(positions[i + 1] == (positions[i][0] + 1, positions[i][1] + 1)
156
- for i in range(len(positions) - 1))
157
- print(f" single particle stays a single particle: {'OK' if ok else 'FAIL'}")
158
- print(f" and moves ballistically on the diagonal, +(1,1) per step: "
159
- f"{'OK' if steady else 'FAIL'} trace={positions[:5]}")
160
- return ok and steady
161
-
162
-
163
- def interaction_gate(a: int, b: int) -> dict:
164
- """One billiard-ball collision as a reversible interaction gate. Input
165
- particle A enters at (2,2) moving SE and B at (7,7) moving NW; at step 4
166
- three output cells carry A&B (the deflected paths), A&~B and ~A&B (the
167
- straight-through paths). AND plus routing by mirrors is functionally
168
- complete for the billiard-ball construction (Margolus 1984)."""
169
- H = W = 12
170
- g = [[0] * W for _ in range(H)]
171
- if a:
172
- g[2][2] = 1
173
- if b:
174
- g[7][7] = 1
175
- g = run(g, 4)
176
- return {"A_and_B": g[3][6], "A_and_notB": g[6][6], "notA_and_B": g[3][3]}
177
-
178
-
179
- def and_gate(a: int, b: int) -> int:
180
- return interaction_gate(a, b)["A_and_B"]
181
-
182
-
183
- def _test_gate():
184
- ok = True
185
- for a in (0, 1):
186
- for b in (0, 1):
187
- o = interaction_gate(a, b)
188
- ok &= (o["A_and_B"] == (a & b) and o["A_and_notB"] == (a & (1 - b))
189
- and o["notA_and_B"] == ((1 - a) & b))
190
- print(f" billiard-ball interaction gate (A&B, A&~B, ~A&B) over all 4 inputs: "
191
- f"{'OK' if ok else 'FAIL'}")
192
- return ok
193
-
194
-
195
- def and3(a: int, b: int, c: int) -> int:
196
- """Two composed collisions. A at (2,2) and B at (7,7) collide into an A&B
197
- particle, which then collides with C launched at (0,9); the output cell
198
- (3,5) at step 4 is occupied iff a, b and c. Composing gates this way builds
199
- arbitrary circuits from the interaction gate."""
200
- H = W = 16
201
- g = [[0] * W for _ in range(H)]
202
- if a:
203
- g[2][2] = 1
204
- if b:
205
- g[7][7] = 1
206
- if c:
207
- g[0][9] = 1
208
- return run(g, 4)[3][5]
209
-
210
-
211
- def _test_compose():
212
- ok = all(and3(a, b, c) == (a & b & c)
213
- for a in (0, 1) for b in (0, 1) for c in (0, 1))
214
- print(f" composed 3-input AND (two chained collisions) over all 8 inputs: "
215
- f"{'OK' if ok else 'FAIL'}")
216
- return ok
217
-
218
-
219
- def _test_collision():
220
- # Two particles interact (the joint evolution differs from independent
221
- # motion) and the collision stays reversible: the physics that logic needs.
222
- H = W = 12
223
- interacted = False
224
- revok = True
225
- # converging pairs: an SE-mover (even,even) meets an NW-mover (odd,odd) on a
226
- # shared diagonal, forming the 1001/0110 diagonal pair the rule deflects.
227
- for a, b in [((2, 2), (7, 7)), ((3, 3), (8, 8)), ((2, 8), (7, 3)),
228
- ((4, 4), (9, 9)), ((2, 2), (9, 9))]:
229
- g = [[0] * W for _ in range(H)]
230
- g[a[0]][a[1]] = 1
231
- g[b[0]][b[1]] = 1
232
- ga = [[0] * W for _ in range(H)]
233
- ga[a[0]][a[1]] = 1
234
- gb = [[0] * W for _ in range(H)]
235
- gb[b[0]][b[1]] = 1
236
- joint = g
237
- for n in range(12):
238
- joint = step(joint, n & 1)
239
- ga = step(ga, n & 1)
240
- gb = step(gb, n & 1)
241
- free = _ball_positions(ga) | _ball_positions(gb)
242
- if _ball_positions(joint) != free:
243
- interacted = True
244
- if run_back(run(g, 12), 12) != g:
245
- revok = False
246
- print(f" two-particle collisions interact (joint != independent motion): "
247
- f"{'OK' if interacted else 'FAIL'}")
248
- print(f" collisions remain reversible: {'OK' if revok else 'FAIL'}")
249
- return interacted and revok
250
-
251
-
252
- if __name__ == "__main__":
253
- print("Reversible Margolus cellular automaton")
254
- a = _test_rule()
255
- g = _test_gates()
256
- b = _test_reversibility()
257
- c = _test_ballistic()
258
- d = _test_collision()
259
- e = _test_gate()
260
- f = _test_compose()
261
- print("PASS" if (a and g and b and c and d and e and f) else "FAIL")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/canonical.py DELETED
@@ -1,25 +0,0 @@
1
- """Deterministic safetensors writer: pins tensor order and metadata key order."""
2
- import json, struct
3
- import torch
4
-
5
- _DT = {torch.float32:"F32", torch.float64:"F64", torch.int64:"I64",
6
- torch.int32:"I32", torch.int16:"I16", torch.int8:"I8",
7
- torch.uint8:"U8", torch.bool:"BOOL"}
8
-
9
- def canonical_bytes(tensors, metadata_pairs):
10
- """tensors: list of (name, tensor) in canonical order.
11
- metadata_pairs: list of (key, value) in canonical order."""
12
- header = {}
13
- if metadata_pairs is not None:
14
- header["__metadata__"] = {k: v for k, v in metadata_pairs}
15
- blobs, off = [], 0
16
- for name, t in tensors:
17
- t = t.contiguous()
18
- b = t.numpy().tobytes()
19
- header[name] = {"dtype": _DT[t.dtype], "shape": list(t.shape),
20
- "data_offsets": [off, off + len(b)]}
21
- blobs.append(b); off += len(b)
22
- s = json.dumps(header, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
23
- pad = (-(8 + len(s))) % 8
24
- s = s + b" " * pad
25
- return struct.pack("<Q", len(s)) + s + b"".join(blobs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_c.py DELETED
@@ -1,199 +0,0 @@
1
- """The host on src/netlist_eval.c, an evaluator in C that shares no code with
2
- the Python evaluators.
3
-
4
- The host netlist is read from its serialization with the standard library,
5
- written in the flat form the C evaluator reads, levelized by the construction
6
- of Lemma 2.6 and written again. The C evaluator then checks one step of the
7
- netlist and one step of Lev(N_host) against its own SUBLEQ on random states,
8
- runs the self-describing instance and three generations of the
9
- self-reproducing instance, each instantiated from the bytes the previous one
10
- emitted.
11
-
12
- python src/check_c.py [--cc gcc]
13
- """
14
- import argparse
15
- import concurrent.futures as cf
16
- import hashlib
17
- import json
18
- import os
19
- import struct
20
- import subprocess
21
- import sys
22
- import tempfile
23
- import time
24
- from array import array
25
-
26
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
- SRC = os.path.join(REPO, "src")
29
-
30
-
31
- def runs_path(name: str) -> str:
32
- d = os.path.join(REPO, "paper", "runs")
33
- os.makedirs(d, exist_ok=True)
34
- return os.path.join(d, name)
35
-
36
-
37
- def parse_sigma(data: bytes):
38
- """Biases, predecessor lists and next-state indices from a serialization."""
39
- hlen = struct.unpack("<Q", data[:8])[0]
40
- head = json.loads(data[8:8 + hlen])
41
- T = {}
42
- for k, spec in head.items():
43
- if k == "__metadata__":
44
- continue
45
- lo, hi = spec["data_offsets"]
46
- a = array({"I8": "b", "I16": "h", "I32": "i"}[spec["dtype"]])
47
- a.frombytes(data[8 + hlen + lo:8 + hlen + hi])
48
- if sys.byteorder != "little":
49
- a.byteswap()
50
- T[k] = list(a)
51
- bias, fanin, pred, nxt = T["net.bias"], T["net.fanin"], T["net.pred"], T["net.next"]
52
- units, p = [], 0
53
- for j in range(len(bias)):
54
- units.append((bias[j], [(abs(e) - 1, 1 if e > 0 else -1) for e in pred[p:p + fanin[j]]]))
55
- p += fanin[j]
56
- return len(nxt), units, nxt
57
-
58
-
59
- def write_flat(path, n, units, nxt):
60
- B, F, P = array("i"), array("i"), array("i")
61
- for b, ins in units:
62
- B.append(b)
63
- F.append(len(ins))
64
- for k, w in ins:
65
- P.append(k)
66
- P.append(w)
67
- with open(path, "wb") as f:
68
- f.write(struct.pack("<iii", n, len(units), len(P) // 2))
69
- f.write(B.tobytes() + F.tobytes() + P.tobytes() + array("i", nxt).tobytes())
70
-
71
-
72
- def levelize(n, units, nxt):
73
- """Lemma 2.6: every row of every layer reads only rows of the layer before."""
74
- v = len(units)
75
- live = [False] * (n + v)
76
- stack = [k for k in nxt if k >= 0]
77
- while stack:
78
- s = stack.pop()
79
- if not live[s]:
80
- live[s] = True
81
- if s >= n:
82
- stack.extend(k for k, _ in units[s - n][1])
83
- depth = [0] * (n + v)
84
- for j, (_, ins) in enumerate(units):
85
- depth[n + j] = 1 + max((depth[k] for k, _ in ins), default=0)
86
- d = max(depth[n + j] for j in range(v) if live[n + j])
87
- lam = [0] * (n + v)
88
- for j, (_, ins) in enumerate(units):
89
- if live[n + j]:
90
- for k, _ in ins:
91
- lam[k] = max(lam[k], depth[n + j])
92
- for s in nxt:
93
- lam[s] = d + 1
94
- layers = [[("copy", i) for i in range(n)]]
95
- where = [{i: i for i in range(n)}]
96
- for ell in range(1, d):
97
- rows, pos = [], {}
98
- for s in range(n + v):
99
- if s >= n and live[s] and depth[s] == ell:
100
- pos[s] = len(rows); rows.append(("unit", s))
101
- for s in range(n + v):
102
- if (s < n or live[s]) and depth[s] < ell < lam[s]:
103
- pos[s] = len(rows); rows.append(("copy", s))
104
- layers.append(rows); where.append(pos)
105
- layers.append([("unit" if depth[s] == d else "copy", s) for s in nxt])
106
- base, out_units = [0], []
107
- for ell in range(1, d + 1):
108
- base.append(base[-1] + len(layers[ell - 1]))
109
- for kind, s in layers[ell]:
110
- if kind == "copy":
111
- out_units.append((-1, [(base[ell - 1] + where[ell - 1][s], 1)]))
112
- else:
113
- b, ins = units[s - n]
114
- out_units.append((b, [(base[ell - 1] + where[ell - 1][k], w) for k, w in ins]))
115
- return (n, out_units, [base[d] + i for i in range(n)],
116
- {"depth": d, "rows": sum(len(L) for L in layers[1:]),
117
- "identity_rows": sum(1 for L in layers[1:] for kind, _ in L if kind == "copy"),
118
- "max_width": max(len(L) for L in layers[1:]),
119
- "layer_sizes": [len(L) for L in layers[1:]]})
120
-
121
-
122
- def framed_parts(s: bytes):
123
- a = struct.unpack("<Q", s[:8])[0]
124
- b = struct.unpack("<Q", s[8 + a:16 + a])[0]
125
- return s[8:8 + a], s[16 + a:16 + a + b], s[16 + a + b:]
126
-
127
-
128
- def main() -> int:
129
- ap = argparse.ArgumentParser()
130
- ap.add_argument("--cc", default="gcc")
131
- args = ap.parse_args()
132
- t_start = time.perf_counter()
133
- from selfrep import M_P, M_STAR, describe, field, ser, read_host
134
- work = tempfile.mkdtemp(prefix="netlist_eval_")
135
- exe = os.path.join(work, "netlist_eval")
136
- subprocess.run([args.cc, "-O3", "-march=native", "-fopenmp", "-o", exe,
137
- os.path.join(SRC, "netlist_eval.c")], check=True)
138
- J = lambda *p: os.path.join(work, *p)
139
-
140
- def ev(*a):
141
- r = subprocess.run([exe] + [str(x) for x in a], capture_output=True, text=True)
142
- return r.returncode, r.stdout.strip()
143
-
144
- sigma = read_host()
145
- n, units, nxt = parse_sigma(sigma)
146
- write_flat(J("host.bin"), n, units, nxt)
147
- ln, lunits, lnxt, lev_info = levelize(n, units, nxt)
148
- write_flat(J("lev.bin"), ln, lunits, lnxt)
149
- r_H = describe(sigma)
150
- tau = describe(field(sigma) + field(bytes(M_STAR)))
151
- expect_star = ser(sigma, bytes(M_STAR), tau)
152
- for name, b in (("mP", bytes(M_P)), ("mstar", bytes(M_STAR)), ("rH", r_H), ("tau", tau)):
153
- open(J(name), "wb").write(b)
154
- out = {"lev": lev_info}
155
-
156
- with cf.ThreadPoolExecutor(max_workers=4) as ex:
157
- f_rand = ex.submit(ev, "rand", J("host.bin"), 1 << 20, 1)
158
- f_lrand = ex.submit(ev, "rand", J("lev.bin"), 1 << 20, 2)
159
- f_desc = ex.submit(ev, "run", J("host.bin"), J("mP"), J("rH"), J("desc.out"), "lock")
160
- out["netlist_random"] = f_rand.result()[1]
161
- out["lev_random"] = f_lrand.result()[1]
162
- out["self_describing"] = f_desc.result()[1]
163
- # three generations, each rebuilt from the bytes the previous one emitted
164
- gens = []
165
- net_path, mem_path, tau_path = J("host.bin"), J("mstar"), J("tau")
166
- for g in range(1, 4):
167
- mode = "lock" if g == 1 else "net"
168
- rc, msg = ev("run", net_path, mem_path, tau_path, J(f"gen{g}.out"), mode)
169
- emitted = open(J(f"gen{g}.out"), "rb").read()
170
- gens.append({"generation": g, "mode": mode, "result": msg,
171
- "sha256": hashlib.sha256(emitted).hexdigest(),
172
- "equals_ser": emitted == expect_star})
173
- s_g, m_g, t_g = framed_parts(emitted)
174
- n2, u2, x2 = parse_sigma(s_g)
175
- net_path = J(f"host_g{g + 1}.bin")
176
- write_flat(net_path, n2, u2, x2)
177
- mem_path, tau_path = J(f"m_g{g + 1}"), J(f"tau_g{g + 1}")
178
- open(mem_path, "wb").write(m_g)
179
- open(tau_path, "wb").write(t_g)
180
- out["generations"] = gens
181
- desc_ok = open(J("desc.out"), "rb").read() == sigma
182
- ok = (out["netlist_random"].endswith("0 halted batches not fixed")
183
- and " 0 disagree" in out["netlist_random"] and " 0 disagree" in out["lev_random"]
184
- and desc_ok and all(g["equals_ser"] for g in gens))
185
- out["self_describing_equals_sigma"] = desc_ok
186
- out["ok"] = ok
187
- out["seconds"] = time.perf_counter() - t_start
188
- for k in ("netlist_random", "lev_random", "self_describing"):
189
- print(f" {k}: {out[k]}")
190
- for g in gens:
191
- print(f" generation {g['generation']} ({g['mode']}): {g['result']}; equals ser: "
192
- f"{'yes' if g['equals_ser'] else 'NO'}; {g['sha256'][:16]}")
193
- json.dump(out, open(runs_path("paper_c_evaluator.json"), "w"), indent=1)
194
- print("C EVALUATOR:", "PASS" if ok else "FAIL")
195
- return 0 if ok else 1
196
-
197
-
198
- if __name__ == "__main__":
199
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_codec.py DELETED
@@ -1,128 +0,0 @@
1
- """The recipe codec on every short string and at every boundary of the format.
2
-
3
- Checked for both encoders: the round trip, the length bound of the completeness
4
- lemma, and that the reserved tag 128 is never emitted. The boundaries covered
5
- are the 127-byte literal cap and the run lengths that map to the extreme repeat
6
- tags, together with exhaustive coverage at small lengths.
7
- """
8
- import itertools
9
- import json
10
- import os
11
- import random
12
- import sys
13
-
14
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
- from selfrep import describe, describe_literal, decode
16
-
17
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18
-
19
-
20
- def runs_path(name: str) -> str:
21
- d = os.path.join(REPO, "paper", "runs")
22
- os.makedirs(d, exist_ok=True)
23
- return os.path.join(d, name)
24
-
25
-
26
- def bound(n: int) -> int:
27
- return n + -(-n // 127) + 1
28
-
29
-
30
- def tags(tape: bytes):
31
- i, out = 0, []
32
- while True:
33
- t = tape[i]
34
- out.append(t)
35
- i += 1
36
- if t == 0:
37
- return out
38
- i += t if t <= 127 else 1
39
-
40
-
41
- class Audit:
42
- def __init__(self):
43
- self.n = 0
44
- self.roundtrip = []
45
- self.overlong = []
46
- self.not_tight = []
47
- self.tag128 = []
48
-
49
- def check(self, b: bytes) -> None:
50
- self.n += 1
51
- for enc in (describe, describe_literal):
52
- r = enc(b)
53
- if decode(r) != b:
54
- self.roundtrip.append((enc.__name__, len(b)))
55
- if len(r) > bound(len(b)):
56
- self.overlong.append((enc.__name__, len(b), len(r)))
57
- if len(describe_literal(b)) != bound(len(b)):
58
- self.not_tight.append(len(b))
59
- if 128 in tags(describe(b)):
60
- self.tag128.append(len(b))
61
-
62
- @property
63
- def ok(self) -> bool:
64
- return not (self.roundtrip or self.overlong or self.not_tight or self.tag128)
65
-
66
-
67
- def main() -> int:
68
- a = Audit()
69
- for n in range(0, 15):
70
- for t in itertools.product((0, 1), repeat=n):
71
- a.check(bytes(t))
72
- binary = a.n
73
- for n in range(0, 10):
74
- for t in itertools.product((0, 1, 2), repeat=n):
75
- a.check(bytes(t))
76
- ternary = a.n - binary
77
- print(f" exhaustive: {binary:,} binary strings to length 14, "
78
- f"{ternary:,} ternary to length 9")
79
-
80
- edges = (list(range(0, 12))
81
- + [126, 127, 128, 129, 130, 251, 252, 253, 254, 255, 256, 257,
82
- 380, 381, 382, 508, 509, 510])
83
- before = a.n
84
- for L in edges:
85
- a.check(bytes([0x41]) * L)
86
- a.check(bytes([0x00]) * L)
87
- a.check(bytes([0xFF]) * L)
88
- a.check(bytes(range(256))[:L] if L <= 256 else bytes(range(256)) * (L // 256))
89
- a.check(bytes([0x41] * L + [0x42]))
90
- a.check(bytes([0x42] + [0x41] * L))
91
- a.check(bytes([0x41] * L + [0x42] * L))
92
- a.check(bytes(([0x41] * 4 + [0x42]) * max(1, L)))
93
- a.check(bytes(([0x41] * 3 + [0x42]) * max(1, L)))
94
- print(f" boundaries: {a.n - before:,} strings straddling the literal cap, "
95
- f"the extreme repeat tags, and the run/literal transitions")
96
-
97
- rng = random.Random(11)
98
- before = a.n
99
- for _ in range(4000):
100
- n = rng.randrange(0, 900)
101
- alpha = rng.choice([2, 3, 7, 256])
102
- a.check(bytes(rng.randrange(alpha) for _ in range(n)))
103
- for _ in range(1500):
104
- n = rng.randrange(0, 900)
105
- out = bytearray()
106
- while len(out) < n:
107
- out += bytes([rng.randrange(256)]) * rng.choice([1, 2, 3, 4, 5, 126, 127, 128, 253])
108
- a.check(bytes(out[:n]))
109
- print(f" random and run-structured: {a.n - before:,} strings")
110
-
111
- print(f" total {a.n:,}: round-trip exact, neither encoder exceeds "
112
- f"|f| + ceil(|f|/127) + 1, the literal encoder attains it, "
113
- f"tag 128 never emitted: {'yes' if a.ok else 'NO'}")
114
- if not a.ok:
115
- print(" roundtrip:", a.roundtrip[:5])
116
- print(" overlong:", a.overlong[:5])
117
- print(" not tight:", a.not_tight[:5])
118
- print(" tag128:", a.tag128[:5])
119
- json.dump({"strings": a.n, "roundtrip_failures": len(a.roundtrip),
120
- "bound_failures": len(a.overlong),
121
- "literal_not_tight": len(a.not_tight),
122
- "tag128_emitted": len(a.tag128)},
123
- open(runs_path("paper_codec.json"), "w"), indent=1)
124
- return 0 if a.ok else 1
125
-
126
-
127
- if __name__ == "__main__":
128
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_device.py DELETED
@@ -1,118 +0,0 @@
1
- """The device against Definition 2.8 of the paper.
2
-
3
- Definition 2.8 specifies one device application on the
4
- post-instruction state (x, tau, h, omega):
5
-
6
- (i) if y[c_wr] = 1 then omega' = omega . y[R_out] and x'[R_out] = 0,
7
- otherwise omega' = omega;
8
- (ii) if y[c_rd] = 1 and h < |tau| then x'[R_in] = tau[h], x'[c_eot] = 0,
9
- h' = h + 1; if y[c_rd] = 1 and h = |tau| then x'[c_eot] = 1 and h' = h;
10
- if y[c_rd] = 0 then h' = h;
11
- (iii) if y[c_rw] = 1 then h' = 0;
12
- (iv) finally x'[c_rd] = x'[c_rw] = x'[c_wr] = 0.
13
-
14
- The clauses are transcribed here independently of selfrep.Tape and the two are
15
- compared over the whole cross product of request values, head positions and
16
- tapes. The sweep covers non-unit values, so it also decides whether a request
17
- fires on the value 1 alone.
18
- """
19
- import itertools
20
- import json
21
- import os
22
- import sys
23
-
24
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
25
- from selfrep import Tape, C_WR, C_EOT, C_RW, C_RD, R_IN, R_OUT
26
-
27
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
-
29
-
30
- def runs_path(name: str) -> str:
31
- d = os.path.join(REPO, "paper", "runs")
32
- os.makedirs(d, exist_ok=True)
33
- return os.path.join(d, name)
34
-
35
-
36
- def spec(mem, tau, h, omega):
37
- """Definition 2.8, clause by clause, on (mem, h, omega)."""
38
- m = list(mem)
39
- w = list(omega)
40
- if m[C_WR] == 1: # (i)
41
- w.append(m[R_OUT])
42
- m[R_OUT] = 0
43
- hp = h
44
- if m[C_RD] == 1: # (ii)
45
- if h < len(tau):
46
- m[R_IN] = tau[h]
47
- m[C_EOT] = 0
48
- hp = h + 1
49
- else:
50
- m[C_EOT] = 1
51
- if m[C_RW] == 1: # (iii)
52
- hp = 0
53
- m[C_WR] = m[C_RD] = m[C_RW] = 0 # (iv)
54
- return m, hp, w
55
-
56
-
57
- def main() -> int:
58
- tapes = [b"", b"\x07", b"\x01\x02", bytes(range(5)), bytes(range(64))]
59
- vals = (0, 1, 2, 3, 127, 128, 255)
60
- bad, n = [], 0
61
- for tau in tapes:
62
- heads = sorted({0, 1, max(0, len(tau) - 1), len(tau)})
63
- for h in heads:
64
- for wr, rd, rw in itertools.product(vals, repeat=3):
65
- for eot, rin, rout in ((0, 0, 0), (1, 9, 200), (0, 255, 1), (1, 1, 255)):
66
- mem = [0] * 256
67
- mem[C_WR], mem[C_RD], mem[C_RW] = wr, rd, rw
68
- mem[C_EOT], mem[R_IN], mem[R_OUT] = eot, rin, rout
69
- mem[0x10] = 0xAB # an ordinary cell, must not move
70
- want_mem, want_h, want_out = spec(mem, tau, h, b"")
71
-
72
- dev = Tape(tau)
73
- dev.h = h
74
- got_mem = list(mem)
75
- dev.apply(got_mem)
76
- n += 1
77
- if (got_mem != want_mem or dev.h != want_h
78
- or bytes(dev.out) != bytes(want_out)):
79
- bad.append({"tau": len(tau), "h": h, "wr": wr, "rd": rd,
80
- "rw": rw, "eot": eot,
81
- "spec": (want_mem[C_EOT], want_mem[R_IN],
82
- want_mem[R_OUT], want_h, list(want_out)),
83
- "impl": (got_mem[C_EOT], got_mem[R_IN],
84
- got_mem[R_OUT], dev.h, list(dev.out))})
85
- print(f" device applications compared: {n:,}")
86
- print(f" implementation agrees with Definition 2.8 on every one: "
87
- f"{'yes' if not bad else f'NO ({len(bad)} disagreements)'}")
88
- for b in bad[:5]:
89
- print(" ", b)
90
-
91
- # A request fires on the value 1 and on no other value. The head starts off
92
- # zero so that a rewind is observable; without that a rewind of a head
93
- # already at 0 changes nothing and the test would not see it fire.
94
- fires = []
95
- for cell in (C_WR, C_RD, C_RW):
96
- for v in range(256):
97
- mem = [0] * 256
98
- mem[cell] = v
99
- mem[R_OUT] = 0x5A
100
- dev = Tape(b"\x11\x22")
101
- dev.h = 1
102
- before = (dev.h, len(dev.out))
103
- dev.apply(mem)
104
- acted = (dev.h, len(dev.out)) != before or mem[R_IN] != 0 or mem[C_EOT] != 0
105
- if acted != (v == 1):
106
- fires.append((cell, v, acted))
107
- print(f" each request fires exactly on the value 1, over all 256 cell values: "
108
- f"{'yes' if not fires else f'NO {fires[:5]}'}")
109
-
110
- ok = not bad and not fires
111
- json.dump({"applications": n, "disagreements": len(bad),
112
- "value_trigger_failures": len(fires)},
113
- open(runs_path("paper_device.json"), "w"), indent=1)
114
- return 0 if ok else 1
115
-
116
-
117
- if __name__ == "__main__":
118
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_environment.py DELETED
@@ -1,110 +0,0 @@
1
- """The cost of instantiation against the cost of the network.
2
-
3
- One generation of the host is: inst splits the emitted bytes, sigma^-1 parses
4
- the first field into the netlist, Lev places the rows and entries of the
5
- layered map, and the device is applied once per step. This script counts those
6
- operations, one per byte read by inst and by sigma^-1, one per row and one per
7
- nonzero entry placed by Lev, and one per device application, times each of
8
- them, and compares the total with the T * M unit evaluations of the run.
9
- """
10
- import json
11
- import os
12
- import sys
13
- import time
14
-
15
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
-
17
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18
-
19
-
20
- def runs_path(name: str) -> str:
21
- d = os.path.join(REPO, "paper", "runs")
22
- os.makedirs(d, exist_ok=True)
23
- return os.path.join(d, name)
24
-
25
-
26
- def main() -> int:
27
- from selfrep import (M_STAR, inst, ser, tau_star, run_reference, read_host,
28
- host_netlist, sigma_host)
29
- from netlist_io import net_of_sigma
30
- from matrix8 import compile_net
31
-
32
- sigma_bytes = read_host()
33
- tau = tau_star(sigma_bytes, M_STAR)
34
- s = ser(sigma_bytes, bytes(M_STAR), tau)
35
-
36
- # sigma: parameters and predecessor entries stored, and the time to write
37
- t0 = time.perf_counter()
38
- net, inputs, outputs = host_netlist()
39
- t_build = time.perf_counter() - t0
40
- t0 = time.perf_counter()
41
- encoded = sigma_host()
42
- t_sigma = time.perf_counter() - t0
43
- assert encoded == sigma_bytes
44
- stored, _, _, _ = net_of_sigma(sigma_bytes)
45
- entries = (2 * len(stored.gates) # bias and fan-in
46
- + sum(len(i) for i, _ in stored.gates.values())
47
- + len(outputs))
48
- print(f" sigma: {entries:,} stored entries -> {len(encoded):,} bytes "
49
- f"in {t_sigma * 1e3:.0f} ms ({t_build * 1e3:.0f} ms to assemble "
50
- f"the netlist from source)")
51
-
52
- # inst: one pass over the serialized instance, then sigma^-1 on its first field
53
- t0 = time.perf_counter()
54
- reps = 50
55
- for _ in range(reps):
56
- a, b, c = inst(s)
57
- t_inst = (time.perf_counter() - t0) / reps
58
- assert (a, b, c) == (sigma_bytes, bytes(M_STAR), tau)
59
- t0 = time.perf_counter()
60
- for _ in range(5):
61
- N, I, O, _ = net_of_sigma(a)
62
- t_sigma_inv = (time.perf_counter() - t0) / 5
63
- assert len(N.gates) == len(stored.gates)
64
- print(f" inst: {len(s):,} bytes parsed in {t_inst * 1e3:.2f} ms, and "
65
- f"{len(N.gates):,} units rebuilt from the first field in "
66
- f"{t_sigma_inv * 1e3:.0f} ms")
67
-
68
- # Lev: the units placed, and the time to place them
69
- t0 = time.perf_counter()
70
- layers, info = compile_net(N, I, O)
71
- t_lev = time.perf_counter() - t0
72
- M = sum(int(W.shape[0]) for W, _ in layers)
73
- nnz = sum(int((W != 0).sum()) for W, _ in layers)
74
- print(f" Lev: {len(N.gates):,} units in, {M:,} rows and {nnz:,} nonzero "
75
- f"entries placed over {info['layers']} layers in {t_lev:.1f} s")
76
-
77
- # the device: one application per step, at most one byte moved
78
- t0 = time.perf_counter()
79
- emitted, T = run_reference(M_STAR, tau)
80
- t_run = time.perf_counter() - t0
81
- assert emitted == s
82
- print(f" device: {T:,} applications, {len(emitted):,} bytes emitted "
83
- f"({t_run:.2f} s for the whole reference run)")
84
-
85
- env_ops = len(s) + len(sigma_bytes) + M + nnz + T
86
- closed = 2 * len(sigma_bytes) + len(bytes(M_STAR)) + len(tau) + 16 + M + nnz + T
87
- assert env_ops == closed, (env_ops, closed)
88
- net_ops = T * M
89
- print(f" instantiation: {env_ops:,} elementary operations per generation, "
90
- f"which is the closed form 2|sigma|+|m|+|tau|+16+M+nnz+T")
91
- print(f" network: {net_ops:,} unit evaluations (T = {T:,}, M = {M:,})")
92
- print(f" ratio: {env_ops / net_ops:.3e}")
93
-
94
- out = {"sigma_entries": entries, "sigma_bytes": len(encoded),
95
- "sigma_seconds": t_sigma, "build_seconds": t_build,
96
- "inst_bytes": len(s), "inst_seconds": t_inst,
97
- "sigma_inverse_seconds": t_sigma_inv,
98
- "lev_units_in": len(N.gates), "lev_units_out": M, "lev_nonzero": nnz,
99
- "lev_layers": info["layers"], "lev_seconds": t_lev,
100
- "lev_entries": info["total_weights"],
101
- "steps": T, "emitted_bytes": len(emitted),
102
- "reference_seconds": t_run,
103
- "environment_ops": env_ops, "network_ops": net_ops,
104
- "ratio": env_ops / net_ops}
105
- json.dump(out, open(runs_path("paper_environment.json"), "w"), indent=1)
106
- return 0
107
-
108
-
109
- if __name__ == "__main__":
110
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_family.py DELETED
@@ -1,64 +0,0 @@
1
- """The universal constructor emits every machine of the artifact, byte for byte.
2
-
3
- The machines are run on the integer reference, one process per machine.
4
-
5
- python src/check_family.py [--jobs N]
6
- """
7
- import argparse, concurrent.futures as cf, hashlib, json, os, sys, time
8
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
- from selfrep import describe, run_reference, M_P
10
-
11
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
-
13
-
14
- def runs_path(name: str) -> str:
15
- d = os.path.join(REPO, "paper", "runs")
16
- os.makedirs(d, exist_ok=True)
17
- return os.path.join(d, name)
18
-
19
-
20
- def files():
21
- out = [os.path.join(REPO, "neural_computer.safetensors")]
22
- v = os.path.join(REPO, "variants")
23
- out += sorted(os.path.join(v, f) for f in os.listdir(v) if f.endswith(".safetensors"))
24
- return out
25
-
26
-
27
- def one(p):
28
- data = open(p, "rb").read()
29
- r = describe(data)
30
- out, steps = run_reference(M_P, r, expect=data)
31
- return {"file": os.path.basename(p), "bytes": len(out), "recipe": len(r),
32
- "steps": steps, "exact": out == data,
33
- "sha256": hashlib.sha256(out).hexdigest()}
34
-
35
-
36
- def main() -> int:
37
- ap = argparse.ArgumentParser()
38
- ap.add_argument("--jobs", type=int, default=os.cpu_count() or 1)
39
- args = ap.parse_args()
40
- t0 = time.perf_counter()
41
- paths = files()
42
- # the largest machines first, so the pool is not left waiting on one of them
43
- order = sorted(paths, key=os.path.getsize, reverse=True)
44
- with cf.ProcessPoolExecutor(max_workers=args.jobs) as ex:
45
- got = dict(zip(order, ex.map(one, order)))
46
- rec = [got[p] for p in paths]
47
- for r in rec:
48
- print(f" {'OK ' if r['exact'] else 'FAIL'} {r['file']:<44} "
49
- f"{r['bytes']:>10,} B {r['steps']:>12,} steps", flush=True)
50
- bad = sum(not r["exact"] for r in rec)
51
- tot_b = sum(r["bytes"] for r in rec)
52
- tot_s = sum(r["steps"] for r in rec)
53
- tot_r = sum(r["recipe"] for r in rec)
54
- dt = time.perf_counter() - t0
55
- print(f"{len(rec)} machines, {tot_b:,} bytes emitted in {tot_s:,} steps "
56
- f"from {tot_r:,} bytes of recipe ({dt/60:.1f} min); failures: {bad}")
57
- json.dump({"machines": rec, "bytes": tot_b, "steps": tot_s,
58
- "recipe_bytes": tot_r, "failures": bad, "seconds": dt},
59
- open(runs_path("paper_runs_family.json"), "w"), indent=1)
60
- return 0 if bad == 0 else 1
61
-
62
-
63
- if __name__ == "__main__":
64
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_family_gate.py DELETED
@@ -1,70 +0,0 @@
1
- """The constructor emitting other machines of the family on the netlist.
2
-
3
- check_family.py runs the constructor on all twenty-eight serialized machines,
4
- but on the integer reference. The claim that the netlist emits them is stronger
5
- than the claim that the reference does, so the machines small enough to finish
6
- are run here on the netlist that sigma(N_host) denotes, unit by unit, with each
7
- emitted byte compared against the target as it is produced.
8
- """
9
- import hashlib
10
- import json
11
- import os
12
- import sys
13
- import time
14
-
15
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
- from selfrep import NetEvaluator, describe, read_host, run_reference, M_P
17
-
18
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
- TARGETS = ["neural_tile.safetensors", "neural_ca.safetensors",
20
- "neural_reversible.safetensors", "neural_attractor.safetensors"]
21
-
22
-
23
- def runs_path(name: str) -> str:
24
- d = os.path.join(REPO, "paper", "runs")
25
- os.makedirs(d, exist_ok=True)
26
- return os.path.join(d, name)
27
-
28
-
29
- def main() -> int:
30
- import torch
31
- device = "cuda" if torch.cuda.is_available() else "cpu"
32
- F = NetEvaluator(read_host(), device=device,
33
- graph=device.startswith("cuda"))
34
- rows = []
35
- bad = 0
36
- total_b = total_s = 0
37
- t_all = time.perf_counter()
38
- for name in TARGETS:
39
- path = os.path.join(REPO, "variants", name)
40
- data = open(path, "rb").read()
41
- r = describe(data)
42
- ref_out, ref_steps = run_reference(M_P, r, expect=data)
43
- assert ref_out == data
44
- t0 = time.perf_counter()
45
- out, steps = F.run(M_P, r, max_steps=ref_steps + 8, expect=data,
46
- progress=200000)
47
- dt = time.perf_counter() - t0
48
- ok = out == data and steps == ref_steps
49
- bad += not ok
50
- total_b += len(out)
51
- total_s += steps
52
- rows.append({"file": name, "bytes": len(out), "recipe": len(r),
53
- "steps": steps, "reference_steps": ref_steps,
54
- "exact": ok, "seconds": dt,
55
- "steps_per_second": steps / dt if dt else None,
56
- "sha256": hashlib.sha256(out).hexdigest()})
57
- print(f" {'OK ' if ok else 'FAIL'} {name:<34} {len(out):>9,} B "
58
- f"{steps:>10,} steps ({dt / 60:.1f} min, "
59
- f"{steps / dt:,.0f} steps/s)", flush=True)
60
- dt = time.perf_counter() - t_all
61
- print(f" {len(rows)} machines on the netlist of sigma: {total_b:,} bytes in "
62
- f"{total_s:,} steps ({dt / 60:.1f} min); failures: {bad}")
63
- json.dump({"machines": rows, "bytes": total_b, "steps": total_s,
64
- "failures": bad, "seconds": dt},
65
- open(runs_path("paper_runs_family_gate.json"), "w"), indent=1)
66
- return 0 if bad == 0 else 1
67
-
68
-
69
- if __name__ == "__main__":
70
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_framing.py DELETED
@@ -1,133 +0,0 @@
1
- """The framing maps ser and inst: round trip, injectivity, and rejection.
2
-
3
- Lemma 2.14 of the paper turns on two properties of Definition 2.12: that the
4
- length fields make the decomposition of ser(Omega) unique, so that ser is
5
- injective and inst inverts it, and that inst is partial, rejecting a string that
6
- is not a framed instance, in place of returning some other triple. Both are
7
- checked here on shapes chosen to stress the framing: empty serializations and
8
- tapes, payloads whose leading bytes look like length fields, memory images at
9
- both extremes, and every truncation of a real instance.
10
- """
11
- import hashlib
12
- import json
13
- import os
14
- import random
15
- import struct
16
- import sys
17
-
18
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
- from selfrep import ser, inst, lam, HOST_PATH, M_STAR, tau_star
20
-
21
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
-
23
-
24
- def runs_path(name: str) -> str:
25
- d = os.path.join(REPO, "paper", "runs")
26
- os.makedirs(d, exist_ok=True)
27
- return os.path.join(d, name)
28
-
29
-
30
- def shapes():
31
- rng = random.Random(5)
32
- host = open(HOST_PATH, "rb").read()
33
- m0 = bytes(256)
34
- m1 = bytes([255]) * 256
35
- mr = bytes(rng.randrange(256) for _ in range(256))
36
- # A payload whose first eight bytes are a plausible length field, and a tape
37
- # that begins the way a framed field begins: if inst read a length from the
38
- # wrong offset these would decode to the wrong split.
39
- trap = lam(1 << 40) + b"trap"
40
- out = [
41
- (b"", m0, b""),
42
- (b"", m0, b"\x00"),
43
- (b"\x00", m0, b""),
44
- (b"\x00" * 3, m1, b""),
45
- (trap, mr, b""),
46
- (b"", mr, trap),
47
- (trap, m0, trap),
48
- (bytes(range(256)), mr, bytes(range(256))),
49
- (host, bytes(M_STAR), tau_star(host, M_STAR)),
50
- (host, m0, b""),
51
- ]
52
- for _ in range(300):
53
- a = bytes(rng.randrange(256) for _ in range(rng.randrange(0, 300)))
54
- t = bytes(rng.randrange(256) for _ in range(rng.randrange(0, 300)))
55
- out.append((a, bytes(rng.randrange(256) for _ in range(256)), t))
56
- return out
57
-
58
-
59
- def main() -> int:
60
- cases = shapes()
61
- rt_bad, digests = [], {}
62
- for sg, m, t in cases:
63
- b = ser(sg, m, t)
64
- if inst(b) != (sg, m, t):
65
- rt_bad.append((len(sg), len(t)))
66
- digests.setdefault(hashlib.sha256(b).hexdigest(), []).append((len(sg), len(m), len(t)))
67
- collisions = {k: v for k, v in digests.items() if len(v) > 1}
68
- print(f" inst(ser(Omega)) = Omega on {len(cases)} instances: "
69
- f"{'exact' if not rt_bad else f'{len(rt_bad)} FAILED'}")
70
- print(f" ser injective over them (distinct instances, distinct bytes): "
71
- f"{'yes' if not collisions else f'NO {list(collisions.values())[:2]}'}")
72
-
73
- # Truncations. A string shorter than the framed serialization and memory
74
- # image is not an instance and must be rejected; one that reaches at least
75
- # that far is the instance with a correspondingly shorter tape, and must
76
- # parse to exactly that.
77
- host = open(HOST_PATH, "rb").read()
78
- tau = tau_star(host, M_STAR)
79
- full = ser(host, bytes(M_STAR), tau)
80
- minimal = 16 + len(host) + 256
81
- probes = sorted({p for p in
82
- [0, 1, 7, 8, 9, 15, 16, 17,
83
- 8 + len(host) - 1, 8 + len(host), 8 + len(host) + 7,
84
- 8 + len(host) + 8, minimal - 256, minimal - 1, minimal,
85
- minimal + 1, len(full) - 1, len(full)]
86
- if 0 <= p <= len(full)})
87
- wrongly_accepted, wrongly_rejected = [], []
88
- for k in probes:
89
- try:
90
- got = inst(full[:k])
91
- except (ValueError, IndexError, struct.error):
92
- if k >= minimal:
93
- wrongly_rejected.append(k)
94
- continue
95
- if k < minimal:
96
- wrongly_accepted.append((k, len(got[1])))
97
- elif got != (host, bytes(M_STAR), full[minimal:k]):
98
- wrongly_accepted.append((k, "wrong triple"))
99
- short = sum(1 for k in probes if k < minimal)
100
- print(f" {short} truncations below a complete memory image rejected: "
101
- f"{'yes' if not wrongly_accepted else f'NO {wrongly_accepted}'}")
102
- print(f" {len(probes) - short} longer prefixes parse to the instance with "
103
- f"that tape: {'yes' if not wrongly_rejected else f'NO {wrongly_rejected}'}")
104
- accepted = wrongly_accepted + wrongly_rejected
105
-
106
- # a memory field of the wrong length must be rejected
107
- wrong = []
108
- for size in (0, 1, 255, 257, 512):
109
- b = lam(len(host)) + host + lam(size) + bytes(size) + tau
110
- try:
111
- inst(b)
112
- wrong.append(size)
113
- except (ValueError, IndexError, struct.error):
114
- pass
115
- print(f" memory field of length != 256 rejected: "
116
- f"{'yes' if not wrong else f'NO for {wrong}'}")
117
-
118
- # the full instance is accepted and recovers exactly
119
- exact = inst(full) == (host, bytes(M_STAR), tau)
120
- print(f" the distributed instance parses back exactly: {exact}")
121
-
122
- ok = not rt_bad and not collisions and not accepted and not wrong and exact
123
- json.dump({"instances": len(cases), "roundtrip_failures": len(rt_bad),
124
- "digest_collisions": len(collisions),
125
- "truncations_accepted": len(accepted),
126
- "bad_memory_lengths_accepted": len(wrong),
127
- "distributed_instance_exact": exact},
128
- open(runs_path("paper_framing.json"), "w"), indent=1)
129
- return 0 if ok else 1
130
-
131
-
132
- if __name__ == "__main__":
133
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_host.py DELETED
@@ -1,113 +0,0 @@
1
- """The reference machine against Definition 3.1, and the halt fixed point.
2
-
3
- Definition 3.1 specifies one SUBLEQ step: read A = M[p], B = M[p+1], C = M[p+2],
4
- set M[B] <- M[B] - M[A] mod 256, and set p <- C when the new M[B] is at most 0
5
- in two's complement and p <- p + 3 otherwise, with indices taken modulo 256 and
6
- a halt at p = 255. The clauses are transcribed here independently of
7
- selfrep.run_reference and the two are compared, with the program counter swept
8
- over every value so the wrapped fetches at the top of memory are covered.
9
-
10
- Proposition 3.9 asserts in addition that a halted state is a fixed point of the
11
- transition, because every architectural write is gated by the negation of the
12
- halt bit. That is checked on the levelized map itself, not on the reference.
13
- """
14
- import json
15
- import os
16
- import random
17
- import sys
18
-
19
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
- from selfrep import Tape, HALT_PC
21
-
22
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
-
24
-
25
- def runs_path(name: str) -> str:
26
- d = os.path.join(REPO, "paper", "runs")
27
- os.makedirs(d, exist_ok=True)
28
- return os.path.join(d, name)
29
-
30
-
31
- def spec_step(mem, p):
32
- """Definition 3.1, transcribed."""
33
- A = mem[p % 256]
34
- B = mem[(p + 1) % 256]
35
- C = mem[(p + 2) % 256]
36
- m = list(mem)
37
- r = (m[B % 256] - m[A % 256]) % 256
38
- m[B % 256] = r
39
- two_c = r - 256 if r >= 128 else r
40
- return m, (C if two_c <= 0 else (p + 3) % 256)
41
-
42
-
43
- def impl_step(mem, p):
44
- """The reference of selfrep.run_reference, one step, device detached."""
45
- m = list(mem)
46
- A = m[p]
47
- B = m[(p + 1) & 0xFF]
48
- C = m[(p + 2) & 0xFF]
49
- r = (m[B] - m[A]) & 0xFF
50
- m[B] = r
51
- return m, (C if (r == 0 or r >= 0x80) else (p + 3) & 0xFF)
52
-
53
-
54
- def main() -> int:
55
- rng = random.Random(31)
56
- bad, n = [], 0
57
- mems = [[0] * 256, [255] * 256, list(range(256)), list(reversed(range(256)))]
58
- mems += [[rng.randrange(256) for _ in range(256)] for _ in range(60)]
59
- for mem in mems:
60
- for p in range(256): # every counter, wraps included
61
- if p == HALT_PC:
62
- continue
63
- sm, sp = spec_step(mem, p)
64
- im, ip = impl_step(mem, p)
65
- n += 1
66
- if sm != im or sp != ip:
67
- bad.append((p, sp, ip))
68
- print(f" reference steps compared with Definition 3.1: {n:,}")
69
- print(f" agree on every one: {'yes' if not bad else f'NO {bad[:4]}'}")
70
-
71
- wrapped = sum(1 for p in (254, 255, 253) if p != HALT_PC) * len(mems)
72
- print(f" of which {len(mems) * 3:,} fetch an operand across the top of memory")
73
-
74
- # A halted state is a fixed point of the levelized map. The state is halted
75
- # when the halt bit is set, coordinate 8 of the vector; the bit latches one
76
- # step after the next counter decodes to 255, so setting the counter alone
77
- # does not halt the machine.
78
- from selfrep import LevEvaluator, read_host
79
- import torch
80
- L = LevEvaluator(read_host(), device="cpu", dense=False)
81
- gen = torch.Generator().manual_seed(97)
82
- V = (torch.rand(48, L.N, generator=gen) < 0.5).float()
83
- V[:, 8] = 1.0
84
- fixed = bool((L.step(V) == V).all())
85
- print(f" halted states are fixed points of Lev(N_host), 48 random states: "
86
- f"{'yes' if fixed else 'NO'}")
87
-
88
- # And the bit latches exactly when the next counter is 255: run one step
89
- # from unhalted states and compare the latched bit against the reference.
90
- U = (torch.rand(64, L.N, generator=gen) < 0.5).float()
91
- U[:, 8] = 0.0
92
- W = L.step(U)
93
- latch_bad = 0
94
- for i in range(U.shape[0]):
95
- mem = [int(sum(int(U[i, 9 + j * 8 + k]) << (7 - k) for k in range(8)))
96
- for j in range(256)]
97
- pc = int(sum(int(U[i, k]) << (7 - k) for k in range(8)))
98
- _, npc = impl_step(mem, pc)
99
- if int(W[i, 8]) != (1 if npc == HALT_PC else 0):
100
- latch_bad += 1
101
- print(f" the halt bit latches exactly when the next counter is 255, "
102
- f"64 random states: {'yes' if latch_bad == 0 else f'NO ({latch_bad})'}")
103
- fixed = fixed and latch_bad == 0
104
-
105
- ok = not bad and fixed
106
- json.dump({"steps_compared": n, "disagreements": len(bad),
107
- "halt_fixed_point": fixed},
108
- open(runs_path("paper_host.json"), "w"), indent=1)
109
- return 0 if ok else 1
110
-
111
-
112
- if __name__ == "__main__":
113
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_host_family.py DELETED
@@ -1,65 +0,0 @@
1
- """The host family on the evaluator in C: one step of H_A against SUBLEQ_A.
2
-
3
- For each width A the serialization of H_A is written by host_family.sigma,
4
- parsed with the standard library, written in the flat form of
5
- src/netlist_eval.c, and one step of the netlist is compared with the C
6
- evaluator's own SUBLEQ on uniformly random full states, together with the
7
- halted fixed point.
8
-
9
- python src/check_host_family.py [--widths 3-12] [--states N]
10
- """
11
- import argparse
12
- import json
13
- import os
14
- import subprocess
15
- import sys
16
- import tempfile
17
- import time
18
-
19
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
-
22
-
23
- def runs_path(name: str) -> str:
24
- d = os.path.join(REPO, "paper", "runs")
25
- os.makedirs(d, exist_ok=True)
26
- return os.path.join(d, name)
27
-
28
-
29
- def main() -> int:
30
- ap = argparse.ArgumentParser()
31
- ap.add_argument("--widths", default="3-12")
32
- ap.add_argument("--states", type=int, default=1 << 16)
33
- ap.add_argument("--cc", default="gcc")
34
- args = ap.parse_args()
35
- lo, hi = (int(x) for x in args.widths.split("-"))
36
- from host_family import sigma
37
- from check_c import parse_sigma, write_flat
38
- work = tempfile.mkdtemp(prefix="host_family_")
39
- exe = os.path.join(work, "netlist_eval")
40
- subprocess.run([args.cc, "-O3", "-march=native", "-fopenmp", "-o", exe,
41
- os.path.join(REPO, "src", "netlist_eval.c")], check=True)
42
- rows, ok = [], True
43
- for A in range(lo, hi + 1):
44
- t0 = time.perf_counter()
45
- s = sigma(A)
46
- n, units, nxt = parse_sigma(s)
47
- path = os.path.join(work, f"h{A}.bin")
48
- write_flat(path, n, units, nxt)
49
- N = max(64, args.states >> max(0, A - 10))
50
- r = subprocess.run([exe, "rand", path, str(N), str(A)], capture_output=True, text=True)
51
- line = r.stdout.strip()
52
- good = r.returncode == 0
53
- ok &= good
54
- rows.append({"A": A, "sigma_bytes": len(s), "units": len(units),
55
- "edges": sum(len(i) for _, i in units), "states": N,
56
- "result": line, "ok": good, "seconds": time.perf_counter() - t0})
57
- print(f" A={A}: {len(units):,} units, {len(s):,} bytes; {line}", flush=True)
58
- os.remove(path)
59
- json.dump({"widths": rows, "ok": ok}, open(runs_path("paper_host_family.json"), "w"), indent=1)
60
- print("HOST FAMILY:", "PASS" if ok else "FAIL")
61
- return 0 if ok else 1
62
-
63
-
64
- if __name__ == "__main__":
65
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_host_fold.py DELETED
@@ -1,73 +0,0 @@
1
- """The host family with its decoders folded into the read conjunctions: depth 11.
2
-
3
- Every fetch and read conjunction and every write enable of H_A reads the bits of
4
- its address directly, so a read from memory takes two levels from the address
5
- bits and not three, and the operand fetch and the operand reads each lose one
6
- level. The step of the folded netlist has depth 11 for every A, against 13. It is
7
- written with host_family.build(A, fold=True), serialized, and one step is
8
- compared with the C evaluator's own SUBLEQ on uniformly random full states, as
9
- src/check_host_family.py does for H_A.
10
-
11
- python src/check_host_fold.py [--widths 3-12] [--states N]
12
- """
13
- import argparse
14
- import json
15
- import os
16
- import subprocess
17
- import sys
18
- import tempfile
19
- import time
20
-
21
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
-
24
-
25
- def depth_of(n, units, nxt):
26
- d = [0] * (n + len(units))
27
- for j, (_, ins) in enumerate(units):
28
- d[n + j] = 1 + max((d[k] for k, _ in ins), default=0)
29
- return max(d[s] for s in nxt)
30
-
31
-
32
- def main() -> int:
33
- ap = argparse.ArgumentParser()
34
- ap.add_argument("--widths", default="3-12")
35
- ap.add_argument("--states", type=int, default=1 << 16)
36
- ap.add_argument("--cc", default="gcc")
37
- args = ap.parse_args()
38
- lo, hi = (int(x) for x in args.widths.split("-"))
39
- from host_family import build, sigma_of_net, sigma
40
- from check_c import parse_sigma, write_flat
41
- work = tempfile.mkdtemp(prefix="host_fold_")
42
- exe = os.path.join(work, "netlist_eval")
43
- subprocess.run([args.cc, "-O3", "-march=native", "-fopenmp", "-o", exe,
44
- os.path.join(REPO, "src", "netlist_eval.c")], check=True)
45
- rows, ok = [], True
46
- for A in range(lo, hi + 1):
47
- t0 = time.perf_counter()
48
- net, inputs, outputs = build(A, fold=True)
49
- s = sigma_of_net(net, inputs, outputs, A)
50
- n, units, nxt = parse_sigma(s)
51
- n0, units0, nxt0 = parse_sigma(sigma(A))
52
- path = os.path.join(work, f"f{A}.bin")
53
- write_flat(path, n, units, nxt)
54
- N = max(64, args.states >> max(0, A - 10))
55
- r = subprocess.run([exe, "rand", path, str(N), str(A)], capture_output=True, text=True)
56
- line = r.stdout.strip().splitlines()[-1] if r.stdout.strip() else r.stderr.strip()
57
- good = r.returncode == 0
58
- ok &= good
59
- row = {"A": A, "units": len(units), "edges": sum(len(i) for _, i in units),
60
- "depth": depth_of(n, units, nxt), "depth_unfolded": depth_of(n0, units0, nxt0),
61
- "units_unfolded": len(units0), "edges_unfolded": sum(len(i) for _, i in units0),
62
- "states": N, "result": line, "agrees": good, "seconds": round(time.perf_counter() - t0, 1)}
63
- rows.append(row)
64
- print(f" A={A}: {row['units']:,} units, {row['edges']:,} edges, depth {row['depth']} "
65
- f"(H_A: {row['units_unfolded']:,}, {row['edges_unfolded']:,}, depth {row['depth_unfolded']}); "
66
- f"{line}", flush=True)
67
- json.dump({"rows": rows, "all_agree": ok},
68
- open(os.path.join(REPO, "paper", "runs", "paper_host_fold.json"), "w"), indent=1)
69
- return 0 if ok else 1
70
-
71
-
72
- if __name__ == "__main__":
73
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_lev.py DELETED
@@ -1,121 +0,0 @@
1
- """Lemma 2.6 on the host: the levelized map equals the netlist it came from.
2
-
3
- Lev(N) is asserted to compute, for every state, the next state that N computes.
4
- The netlist here is the one the serialization of the host denotes, evaluated
5
- gate by gate in topological order with no reference to the levelization, and the
6
- two are compared on uniformly random states, which lie off every trajectory of
7
- the constructor.
8
-
9
- The same states carry the structural facts the lemma asserts: every weight of
10
- every layer lies in {-1, 0, 1}, and every pre-activation is an integer, so the
11
- comparator has a margin of one half.
12
- """
13
- import json
14
- import os
15
- import sys
16
-
17
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
- import torch
19
-
20
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
-
22
-
23
- def runs_path(name: str) -> str:
24
- d = os.path.join(REPO, "paper", "runs")
25
- os.makedirs(d, exist_ok=True)
26
- return os.path.join(d, name)
27
-
28
-
29
- def gate_by_gate(net, inputs, outputs, V):
30
- """Evaluate the netlist directly: topological order, one gate at a time."""
31
- gates = net.gates
32
- indeg, cons = {}, {}
33
- for g, (ins, _) in gates.items():
34
- indeg[g] = len([s for s, _ in ins if s in gates])
35
- for s, _ in ins:
36
- cons.setdefault(s, []).append(g)
37
- order = [g for g, d in indeg.items() if d == 0]
38
- i = 0
39
- while i < len(order):
40
- for c in cons.get(order[i], []):
41
- indeg[c] -= 1
42
- if indeg[c] == 0:
43
- order.append(c)
44
- i += 1
45
- assert len(order) == len(gates)
46
-
47
- B = V.shape[0]
48
- val = {"#0": torch.zeros(B), "#1": torch.ones(B)}
49
- for k, name in enumerate(inputs):
50
- val[name] = V[:, k]
51
- for g in order:
52
- ins, bias = gates[g]
53
- acc = torch.full((B,), float(bias))
54
- for s, w in ins:
55
- acc = acc + w * val[s]
56
- val[g] = (acc >= 0).float()
57
- return torch.stack([val[o] for o in outputs], dim=1)
58
-
59
-
60
- def main() -> int:
61
- from netlist_io import net_of_sigma
62
- from selfrep import LevEvaluator, NetEvaluator, read_host
63
-
64
- sigma = read_host()
65
- net, inputs, outputs, _ = net_of_sigma(sigma)
66
- L = LevEvaluator(sigma, device="cpu", dense=False)
67
-
68
- gen = torch.Generator().manual_seed(20260910)
69
- B = 96
70
- V = (torch.rand(B, len(inputs), generator=gen) < 0.5).float()
71
-
72
- direct = gate_by_gate(net, inputs, outputs, V)
73
- levelled = L.step(V)
74
- agree = bool((direct == levelled).all())
75
- print(f" netlist has {len(net.gates):,} gates on {len(inputs):,} state bits")
76
- print(f" gate-by-gate evaluation equals Lev(N_host) on {B} uniformly random "
77
- f"states: {'yes' if agree else 'NO'}")
78
- if not agree:
79
- d = (direct != levelled).nonzero()
80
- print(" first disagreements:", d[:5].tolist())
81
-
82
- grouped = NetEvaluator(sigma, device="cpu").step(V)
83
- agree_net = bool((direct == grouped).all())
84
- print(f" the evaluator that carries only the netlist's own predecessor "
85
- f"entries agrees on the same states: {'yes' if agree_net else 'NO'}")
86
-
87
- # The dense form: ternary weights, integer pre-activations, margin one half.
88
- D = LevEvaluator(sigma, device="cpu", dense=True)
89
- nonternary = [i for i, W in enumerate(D.W)
90
- if not set(torch.unique(W).tolist()) <= {-1.0, 0.0, 1.0}]
91
- nonint = [i for i, b in enumerate(D.B) if not bool((b == b.round()).all())]
92
- print(f" every weight of all {len(D.W)} layers lies in {{-1,0,1}}: "
93
- f"{'yes' if not nonternary else f'NO at {nonternary[:3]}'}")
94
- print(f" every bias is an integer: {'yes' if not nonint else f'NO at {nonint[:3]}'}")
95
-
96
- y, worst, frac = V, float("inf"), 0.0
97
- for W, b in zip(D.W, D.B):
98
- pre = y @ W.T + b
99
- worst = min(worst, float((pre + 0.5).abs().min()))
100
- frac = max(frac, float((pre - pre.round()).abs().max()))
101
- y = (pre >= 0).float()
102
- same_dense = bool((y == levelled).all())
103
- print(f" dense stack agrees with the sparse evaluation on the same states: "
104
- f"{'yes' if same_dense else 'NO'}")
105
- print(f" minimum distance of a pre-activation from -1/2 over all "
106
- f"{len(D.W)} layers: {worst:.6f}")
107
- print(f" maximum distance of a pre-activation from an integer: {frac:.3e}")
108
-
109
- ok = (agree and agree_net and same_dense and not nonternary
110
- and not nonint and abs(worst - 0.5) < 1e-9)
111
- json.dump({"states": B, "gates": len(net.gates), "agree": agree,
112
- "agree_grouped": agree_net,
113
- "dense_agree": same_dense, "nonternary_layers": len(nonternary),
114
- "noninteger_bias_layers": len(nonint),
115
- "min_margin": worst, "max_noninteger": frac},
116
- open(runs_path("paper_lev.json"), "w"), indent=1)
117
- return 0 if ok else 1
118
-
119
-
120
- if __name__ == "__main__":
121
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_optimal.py DELETED
@@ -1,153 +0,0 @@
1
- """The length bound of the completeness lemma is optimal for the token format.
2
-
3
- A literal token covering c bytes occupies c + 1 bytes and a repeat token
4
- covering a run of c equal bytes occupies 2, so the shortest recipe for a given
5
- output is a shortest path over the positions of that output. The exact optimum
6
- is computed here by that recurrence and compared with |f| + ceil(|f|/127) + 1
7
- on strings with no two equal adjacent bytes, where no repeat token can cover
8
- more than one byte and the bound is therefore attained by every shortest
9
- recipe, and with the encoders of the artifact on arbitrary strings.
10
- """
11
- import itertools
12
- import json
13
- import os
14
- import random
15
- import sys
16
-
17
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
- from selfrep import describe, describe_literal, decode
19
-
20
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
-
22
-
23
- def runs_path(name: str) -> str:
24
- d = os.path.join(REPO, "paper", "runs")
25
- os.makedirs(d, exist_ok=True)
26
- return os.path.join(d, name)
27
-
28
-
29
- def bound(n: int) -> int:
30
- return n + -(-n // 127) + 1
31
-
32
-
33
- def shortest(f: bytes) -> int:
34
- """Length of a shortest r in R with delta(r) = f, by dynamic programming.
35
-
36
- cost[i] is the least number of bytes needed to cover f[i:]; the END byte is
37
- added once at the end. A literal token may carry 1..127 bytes at a cost of
38
- one more than it carries; a repeat token carries 1..127 copies of one byte
39
- at a cost of two.
40
- """
41
- n = len(f)
42
- INF = float("inf")
43
- cost = [INF] * (n + 1)
44
- cost[n] = 0
45
- for i in range(n - 1, -1, -1):
46
- best = INF
47
- for c in range(1, min(127, n - i) + 1):
48
- v = cost[i + c] + c + 1 # literal token
49
- if v < best:
50
- best = v
51
- run = 0
52
- while run < min(127, n - i) and f[i + run] == f[i]:
53
- run += 1
54
- v = cost[i + run] + 2 # repeat token
55
- if v < best:
56
- best = v
57
- cost[i] = best
58
- return cost[0] + 1
59
-
60
-
61
- def run_free(f: bytes) -> bool:
62
- return all(f[i] != f[i + 1] for i in range(len(f) - 1))
63
-
64
-
65
- def main() -> int:
66
- out = {}
67
- ok = True
68
-
69
- # Exhaustive over ternary strings with no two equal adjacent bytes.
70
- tested = 0
71
- bad = []
72
- for n in range(0, 11):
73
- for t in itertools.product((0, 1, 2), repeat=n):
74
- f = bytes(t)
75
- if not run_free(f):
76
- continue
77
- tested += 1
78
- s = shortest(f)
79
- if s != bound(n) or len(describe_literal(f)) != bound(n):
80
- bad.append(n)
81
- print(f" every ternary string of length at most 10 with no two equal "
82
- f"adjacent bytes ({tested:,} of them): the shortest recipe has "
83
- f"exactly |f| + ceil(|f|/127) + 1 bytes: "
84
- f"{'yes' if not bad else f'NO at lengths {sorted(set(bad))[:5]}'}")
85
- ok &= not bad
86
- out["run_free_exhaustive"] = tested
87
- out["run_free_failures"] = len(bad)
88
-
89
- # Longer run-free strings over the full alphabet, including the lengths at
90
- # which a further literal token becomes necessary.
91
- rng = random.Random(4)
92
- lens = [0, 1, 126, 127, 128, 253, 254, 255, 380, 381, 508, 509, 700]
93
- lens += [rng.randrange(0, 900) for _ in range(120)]
94
- lbad = []
95
- for n in lens:
96
- f = bytearray()
97
- prev = -1
98
- for _ in range(n):
99
- c = rng.randrange(256)
100
- while c == prev:
101
- c = rng.randrange(256)
102
- f.append(c)
103
- prev = c
104
- f = bytes(f)
105
- if shortest(f) != bound(n) or len(describe_literal(f)) != bound(n):
106
- lbad.append(n)
107
- print(f" {len(lens)} further run-free strings, lengths to 900 and at every "
108
- f"multiple of 127: shortest recipe equals the bound: "
109
- f"{'yes' if not lbad else f'NO at {lbad[:5]}'}")
110
- ok &= not lbad
111
- out["run_free_sampled"] = len(lens)
112
- out["run_free_sampled_failures"] = len(lbad)
113
-
114
- # On arbitrary strings the bound is an upper bound only; record how far the
115
- # encoder of the artifact is from the optimum.
116
- gaps = []
117
- abad = 0
118
- for _ in range(300):
119
- n = rng.randrange(0, 600)
120
- alpha = rng.choice([1, 2, 3, 9, 256])
121
- f = bytes(rng.randrange(alpha) for _ in range(n))
122
- s = shortest(f)
123
- d = len(describe(f))
124
- if decode(describe(f)) != f or d < s or d > bound(n):
125
- abad += 1
126
- gaps.append(d - s)
127
- print(f" 300 arbitrary strings: describe is never shorter than the optimum "
128
- f"and never longer than the bound: {'yes' if abad == 0 else 'NO'}; "
129
- f"excess over the optimum: max {max(gaps)}, mean {sum(gaps)/len(gaps):.2f}")
130
- ok &= abad == 0
131
- out["arbitrary_strings"] = len(gaps)
132
- out["arbitrary_failures"] = abad
133
- out["describe_excess_max"] = max(gaps)
134
- out["describe_excess_mean"] = sum(gaps) / len(gaps)
135
-
136
- # The serialization of the host is not run-free, so its optimum is shorter.
137
- from selfrep import HOST_PATH
138
- sigma = open(HOST_PATH, "rb").read()
139
- s = shortest(sigma)
140
- print(f" sigma(N_host): {len(sigma):,} bytes; bound {bound(len(sigma)):,}, "
141
- f"describe {len(describe(sigma)):,}, shortest recipe {s:,}")
142
- out["sigma_bytes"] = len(sigma)
143
- out["sigma_bound"] = bound(len(sigma))
144
- out["sigma_describe"] = len(describe(sigma))
145
- out["sigma_shortest"] = s
146
- ok &= s <= len(describe(sigma)) <= bound(len(sigma))
147
-
148
- json.dump(out, open(runs_path("paper_optimal.json"), "w"), indent=1)
149
- return 0 if ok else 1
150
-
151
-
152
- if __name__ == "__main__":
153
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_rocq.py DELETED
@@ -1,67 +0,0 @@
1
- """Build the Rocq development, print the assumptions of its main theorems, and compare
2
- the extracted netlist with the generator's.
3
-
4
- The development in rocq/ proves that the two-head netlist computes the step of the
5
- machine from every state at every size, that the organism builds every record image
6
- from any description of it, and that it reproduces in every generation; it also
7
- evaluates three generations in the kernel. Run under an environment that provides
8
- rocq and ocamlfind, for instance `eval $(opam env)`:
9
-
10
- python src/check_rocq.py
11
- """
12
- import json
13
- import os
14
- import re
15
- import shutil
16
- import subprocess
17
- import sys
18
- import time
19
-
20
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
- ROCQ = os.path.join(REPO, 'rocq')
22
- RECORD = os.path.join(REPO, 'paper', 'runs', 'paper_rocq.json')
23
-
24
-
25
- def main():
26
- for tool in ('rocq', 'make', 'ocamlfind'):
27
- if shutil.which(tool) is None:
28
- print(f'{tool} not found on PATH')
29
- return 1
30
- version = subprocess.run(['rocq', '--version'], capture_output=True, text=True).stdout.strip()
31
- t0 = time.time()
32
- subprocess.run(['rocq', 'makefile', '-f', '_CoqProject', '-o', 'Makefile'], cwd=ROCQ, check=True)
33
- subprocess.run(['make', 'clean'], cwd=ROCQ, check=True, capture_output=True)
34
- jobs = str(os.cpu_count() or 1)
35
- if subprocess.run(['make', '-j', jobs], cwd=ROCQ).returncode != 0:
36
- print('the build failed')
37
- return 1
38
- build = time.time() - t0
39
- out = subprocess.run(['rocq', 'compile', '-R', '.', 'TH', 'Assumptions.v'], cwd=ROCQ,
40
- capture_output=True, text=True)
41
- for f in os.listdir(ROCQ):
42
- if f.startswith('Assumptions.') and f != 'Assumptions.v' or f == '.Assumptions.aux':
43
- os.remove(os.path.join(ROCQ, f))
44
- if out.returncode != 0:
45
- print(out.stdout, out.stderr)
46
- return 1
47
- names = re.findall(r'^Print Assumptions (\S+)\.$', open(os.path.join(ROCQ, 'Assumptions.v')).read(), re.M)
48
- lines = [l.strip() for l in out.stdout.splitlines() if l.strip()]
49
- if len(lines) != len(names) or any(l != 'Closed under the global context' for l in lines):
50
- print(out.stdout)
51
- return 1
52
- ext = subprocess.run([sys.executable, os.path.join(REPO, 'src', 'twohead_net.py'), 'extract'],
53
- capture_output=True, text=True)
54
- identical = ext.returncode == 0 and '"identical": true' in ext.stdout
55
- files = [l for l in open(os.path.join(ROCQ, '_CoqProject')).read().split() if l.endswith('.v')]
56
- res = dict(rocq=version.splitlines()[0], files=len(files),
57
- build_seconds=round(build, 1), theorems={n: 'closed' for n in names},
58
- extraction_identical=identical)
59
- rec = json.load(open(RECORD)) if os.path.exists(RECORD) else {}
60
- rec.update(res)
61
- json.dump(rec, open(RECORD, 'w'), indent=1)
62
- print(json.dumps(res, indent=1))
63
- return 0 if identical else 1
64
-
65
-
66
- if __name__ == '__main__':
67
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_sigma.py DELETED
@@ -1,89 +0,0 @@
1
- """The serialization sigma of the ordered host netlist, written two ways.
2
-
3
- Units are stored in the order the family builds them, every unit after its
4
- predecessors; tensor entries are ordered by (element type, name); the metadata
5
- object is written first with its keys in the fixed order (machine,
6
- weight_quantization, signal_registry, io); the JSON uses compact separators and
7
- is padded with spaces so the tensor data begins at an eight-byte boundary.
8
- Rebuilding the host from source reproduces its distributed encoding byte for
9
- byte, the vectorized writer host_family.sigma produces the same bytes without
10
- building the netlist, and the netlist the encoding denotes serializes back to
11
- the same bytes.
12
- """
13
- import json
14
- import os
15
- import struct
16
- import sys
17
-
18
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
- import torch
20
-
21
- from netlist_io import net_of_sigma, sigma_of_net
22
- from selfrep import HOST_PATH, STATE_LAYOUT, IO_CELLS, host_netlist, sigma_host
23
-
24
- _DTORD = {"F64": 0, "F32": 1, "F16": 2, "I64": 3, "I32": 4, "I16": 5, "I8": 6,
25
- "U8": 7, "BOOL": 8}
26
-
27
-
28
- def resigma(data: bytes) -> bytes:
29
- """Serialize the netlist that `data` denotes."""
30
- net, inputs, outputs, meta = net_of_sigma(data)
31
- return sigma_of_net(net, inputs, outputs, meta["machine"],
32
- json.loads(meta["signal_registry"]),
33
- json.loads(meta["io"]))
34
-
35
-
36
- def main() -> int:
37
- rebuilt = sigma_host()
38
- dist = open(HOST_PATH, "rb").read()
39
- same = rebuilt == dist
40
- n = struct.unpack("<Q", dist[:8])[0]
41
- h = json.loads(dist[8:8 + n])
42
- meta = h.pop("__metadata__")
43
- order = list(h)
44
- by_dtype_name = order == sorted(order, key=lambda k: (_DTORD[h[k]["dtype"]], k))
45
- signed_int = all(h[k]["dtype"] in ("I8", "I16", "I32", "I64") for k in order)
46
- print(f"rebuild from source equals the distributed encoding byte for byte: {same}")
47
- print(f" bytes {len(dist):,} header {n:,} tensor entries {len(order)}")
48
- print(f" metadata keys in order: {list(meta)}")
49
- print(f" tensor entries ordered by (element type, name): {by_dtype_name}")
50
- print(f" every element type is a signed integer type: {signed_int}")
51
- print(f" data begins at offset {8+n}, which is 8-aligned: {(8+n) % 8 == 0}")
52
- print(f" header padded with {len(dist[8:8+n]) - len(dist[8:8+n].rstrip(b' '))} spaces")
53
-
54
- again = sigma_host()
55
- print(f" two independent builds agree: {again == rebuilt}")
56
- from host_family import sigma as family_sigma
57
- vec = family_sigma(8) == dist
58
- print(f" the vectorized writer produces the same bytes: {vec}")
59
-
60
- # sigma(sigma^-1(s)) = s on the image: the encoding determines the netlist
61
- # and the netlist determines the encoding.
62
- round_trip = resigma(dist) == dist
63
- print(f" the netlist the encoding denotes serializes back to it: {round_trip}")
64
-
65
- net, inputs, outputs = host_netlist()
66
- built, _, _, _ = net_of_sigma(dist)
67
- entries = sum(len(i) for i, _ in built.gates.values())
68
- print(f" {len(built.gates):,} units and {entries:,} predecessor entries "
69
- f"recovered from the file alone, on {len(inputs):,} state bits")
70
-
71
- # the netlist recovered from the file computes the transition the source
72
- # netlist computes, on states no trajectory visits
73
- from check_lev import gate_by_gate
74
- gen = torch.Generator().manual_seed(41)
75
- V = (torch.rand(64, len(inputs), generator=gen) < 0.5).float()
76
- src = gate_by_gate(net, inputs, outputs, V)
77
- rec_net, rec_in, rec_out, _ = net_of_sigma(dist)
78
- rec = gate_by_gate(rec_net, rec_in, rec_out, V)
79
- agree = bool((src == rec).all())
80
- print(f" it computes the transition of the source netlist on 64 uniformly "
81
- f"random states: {agree}")
82
-
83
- ok = (same and again == rebuilt and vec and round_trip and agree and by_dtype_name
84
- and signed_int)
85
- return 0 if ok else 1
86
-
87
-
88
- if __name__ == "__main__":
89
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_steps.py DELETED
@@ -1,169 +0,0 @@
1
- """Closed forms for the number of steps of the constructor and of the copier.
2
-
3
- Reading a recipe token by token, the decoding loop of P spends a fixed number
4
- of steps on the token's tag and a fixed number on each byte it contributes to
5
- the output, so the whole run is a sum over the token census:
6
-
7
- steps(P, r) = 5 f_lit + 4 f_rep + 7 L + 9 R + 8,
8
-
9
- where L and R count the literal and repeat tokens, f_lit and f_rep the bytes
10
- each kind contributes, and the last term is the end token together with the
11
- halt instruction. The copying phase of P* costs six steps per tape byte and
12
- two to detect the end of tape. The formulas are compared here against the
13
- reference run on the recipes of the paper and on random recipes.
14
- """
15
- import json
16
- import os
17
- import random
18
- import sys
19
-
20
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
- from selfrep import (HOST_PATH, M_P, M_STAR, describe, describe_literal, decode,
22
- run_reference, tau_star, Tape, HALT_PC)
23
-
24
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
25
-
26
-
27
- def runs_path(name: str) -> str:
28
- d = os.path.join(REPO, "paper", "runs")
29
- os.makedirs(d, exist_ok=True)
30
- return os.path.join(d, name)
31
-
32
-
33
- def census(r: bytes):
34
- """(literal tokens, repeat tokens, bytes from each) of a recipe."""
35
- i = 0
36
- lit = rep = f_lit = f_rep = 0
37
- while True:
38
- t = r[i]
39
- i += 1
40
- if t == 0:
41
- return lit, rep, f_lit, f_rep
42
- if t <= 127:
43
- lit += 1
44
- f_lit += t
45
- i += t
46
- else:
47
- rep += 1
48
- f_rep += 256 - t
49
- i += 1
50
-
51
-
52
- def steps_P(r: bytes) -> int:
53
- """Closed form for the run of the constructor P on a recipe."""
54
- lit, rep, f_lit, f_rep = census(r)
55
- return 5 * f_lit + 4 * f_rep + 7 * lit + 9 * rep + 8
56
-
57
-
58
- def steps_P_star(tau: bytes):
59
- """Closed form for the three phases of P* on its own description tape."""
60
- lit, rep, f_lit, f_rep = census(tau)
61
- a = 5 * f_lit + 4 * f_rep + 7 * lit + 9 * rep + 7
62
- return {"A": a, "R": 1, "B": 6 * len(tau) + 2}
63
-
64
-
65
- def upper_bound(r: bytes) -> int:
66
- """5|delta(r)| + 9k + 8 with k the number of tokens, k <= (|r|-1)/2."""
67
- lit, rep, f_lit, f_rep = census(r)
68
- return 5 * (f_lit + f_rep) + 9 * (lit + rep) + 8
69
-
70
-
71
- def coarse_bound(r: bytes) -> int:
72
- return 5 * len(decode(r)) + (9 * (len(r) - 1)) // 2 + 8
73
-
74
-
75
- def phase_split(mem0, tau: bytes):
76
- mem = list(mem0)
77
- dev = Tape(tau)
78
- pc = 0
79
- counts = {"A": 0, "R": 0, "B": 0}
80
- while pc != HALT_PC:
81
- counts["A" if pc < 3 * 20 else ("R" if pc == 3 * 20 else "B")] += 1
82
- A = mem[pc]
83
- B = mem[(pc + 1) & 0xFF]
84
- C = mem[(pc + 2) & 0xFF]
85
- r = (mem[B] - mem[A]) & 0xFF
86
- mem[B] = r
87
- pc = C if (r == 0 or r >= 0x80) else (pc + 3) & 0xFF
88
- dev.apply(mem)
89
- return counts
90
-
91
-
92
- def main() -> int:
93
- out = {}
94
- ok = True
95
- sigma = open(HOST_PATH, "rb").read()
96
-
97
- named = [("r_H = describe(sigma(N_host))", describe(sigma)),
98
- ("the all-literal encoding of sigma(N_host)", describe_literal(sigma)),
99
- ("tau* (the self-reproducing tape)", tau_star(sigma, M_STAR))]
100
- rows = []
101
- for label, r in named:
102
- _, n = run_reference(M_P, r)
103
- pred = steps_P(r)
104
- rows.append({"tape": label, "recipe_bytes": len(r), "output_bytes":
105
- len(decode(r)), "steps": n, "closed_form": pred,
106
- "agrees": n == pred, "upper": upper_bound(r),
107
- "coarse": coarse_bound(r)})
108
- print(f" {label}: {n:,} steps, closed form {pred:,}, "
109
- f"{'agree' if n == pred else 'DISAGREE'}; "
110
- f"bound 5|f|+9k+8 = {upper_bound(r):,}", flush=True)
111
- ok &= n == pred and n <= upper_bound(r) <= coarse_bound(r)
112
- out["named"] = rows
113
-
114
- # random recipes: both encoders, and strings built to force each token kind
115
- rng = random.Random(1009)
116
- bad = 0
117
- tested = 0
118
- for _ in range(400):
119
- n = rng.randrange(0, 700)
120
- alpha = rng.choice([1, 2, 3, 17, 256])
121
- f = bytes(rng.randrange(alpha) for _ in range(n))
122
- for enc in (describe, describe_literal):
123
- r = enc(f)
124
- _, s = run_reference(M_P, r)
125
- tested += 1
126
- if s != steps_P(r) or s > upper_bound(r) or upper_bound(r) > coarse_bound(r):
127
- bad += 1
128
- print(f" {tested:,} random runs of P: closed form exact and within the "
129
- f"bound on every one: {'yes' if bad == 0 else f'NO ({bad})'}", flush=True)
130
- ok &= bad == 0
131
- out["random_runs"] = tested
132
- out["random_failures"] = bad
133
-
134
- # the three phases of P*
135
- tau = tau_star(sigma, M_STAR)
136
- measured = phase_split(M_STAR, tau)
137
- pred = steps_P_star(tau)
138
- agree = measured == pred
139
- print(f" P* phases measured {measured}, closed form {pred}: "
140
- f"{'agree' if agree else 'DISAGREE'}", flush=True)
141
- print(f" total {sum(measured.values()):,} = {sum(pred.values()):,}", flush=True)
142
- ok &= agree
143
- out["phases_measured"] = measured
144
- out["phases_closed_form"] = pred
145
- out["total"] = sum(pred.values())
146
-
147
- # phase B is six steps per tape byte and two more, over recipes of every
148
- # shape; P* is run only on recipes, on which it halts
149
- bbad = 0
150
- probes = 0
151
- for _ in range(40):
152
- for enc in (describe, describe_literal):
153
- probe = enc(bytes(rng.randrange(rng.choice([2, 4, 256]))
154
- for _ in range(rng.randrange(0, 200))))
155
- got = phase_split(M_STAR, probe)
156
- probes += 1
157
- if got != steps_P_star(probe):
158
- bbad += 1
159
- print(f" phase B = 6|tau| + 2 and phase R = 1 on {probes} further recipes: "
160
- f"{'yes' if bbad == 0 else f'NO ({bbad})'}", flush=True)
161
- ok &= bbad == 0
162
- out["phase_b_failures"] = bbad
163
-
164
- json.dump(out, open(runs_path("paper_steps.json"), "w"), indent=1)
165
- return 0 if ok else 1
166
-
167
-
168
- if __name__ == "__main__":
169
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/check_ternarity.py DELETED
@@ -1,142 +0,0 @@
1
- """The branch predicate separates ternary weights from integer weights.
2
-
3
- The host decides a branch from the two's-complement result r by the depth-two
4
- cell OR(r_7, z(r)). With integer weights the same predicate is one unit,
5
- H(128 r_7 - sum_{i<7} 2^i r_i), so the depth-two cell is where the restriction
6
- to {-1,0,1} is paid for. Both halves are checked here: every ternary unit on
7
- the eight result bits is enumerated and none computes the predicate, and the
8
- single large-weight unit is evaluated on all 256 results.
9
- """
10
- import itertools
11
- import json
12
- import os
13
- import sys
14
-
15
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
- import torch
17
-
18
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
-
20
-
21
- def runs_path(name: str) -> str:
22
- d = os.path.join(REPO, "paper", "runs")
23
- os.makedirs(d, exist_ok=True)
24
- return os.path.join(d, name)
25
-
26
-
27
- def predicate(n: int):
28
- """[r <= 0] for r read in two's complement on `n` bits, as a 2^n vector."""
29
- rows = []
30
- for v in range(1 << n):
31
- two_c = v - (1 << n) if v >> (n - 1) else v
32
- rows.append(1 if two_c <= 0 else 0)
33
- return torch.tensor(rows, dtype=torch.long)
34
-
35
-
36
- def bitmatrix(n: int):
37
- """Row v holds the bits r_0 .. r_{n-1} of v, least significant first."""
38
- return torch.tensor([[(v >> i) & 1 for i in range(n)] for v in range(1 << n)],
39
- dtype=torch.long)
40
-
41
-
42
- def ternary_units_computing(n: int, target, bias_range):
43
- """Every (w, b) in {-1,0,1}^n x bias_range whose unit computes `target`."""
44
- X = bitmatrix(n)
45
- found = []
46
- total = 0
47
- for w in itertools.product((-1, 0, 1), repeat=n):
48
- pre = X @ torch.tensor(w, dtype=torch.long)
49
- for b in bias_range:
50
- total += 1
51
- if bool(((pre + b >= 0).long() == target).all()):
52
- found.append((w, b))
53
- return found, total
54
-
55
-
56
- def main() -> int:
57
- out = {}
58
- ok = True
59
-
60
- # The predicate is a threshold function of the eight result bits, realized
61
- # by one unit whose weights are the positional values of the bits.
62
- n = 8
63
- target = predicate(n)
64
- X = bitmatrix(n)
65
- big = torch.tensor([-(1 << i) for i in range(n - 1)] + [1 << (n - 1)],
66
- dtype=torch.long)
67
- exact = bool(((X @ big >= 0).long() == target).all())
68
- print(f" one integer-weight unit with weights "
69
- f"(-1,-2,-4,-8,-16,-32,-64,+128) and bias 0 computes [r <= 0] on all "
70
- f"{1 << n} results: {'yes' if exact else 'NO'}")
71
- ok &= exact
72
- out["integer_unit_exact"] = exact
73
- out["integer_unit_max_weight"] = int(big.abs().max())
74
-
75
- # No ternary unit does, for any integer bias. A bias outside [-n, n] makes
76
- # the unit constant, so the sweep below is exhaustive over all biases.
77
- bias_range = range(-(n + 2), n + 3)
78
- found, total = ternary_units_computing(n, target, bias_range)
79
- print(f" ternary units on the eight result bits enumerated: {total:,} "
80
- f"(all {3 ** n:,} weight vectors, biases {bias_range.start} to "
81
- f"{bias_range.stop - 1})")
82
- print(f" ternary units computing [r <= 0]: {len(found)}"
83
- f"{' (none, as the proposition asserts)' if not found else f' {found[:3]}'}")
84
- ok &= not found
85
- out["ternary_units_enumerated"] = total
86
- out["ternary_units_computing_predicate"] = len(found)
87
- # A unit with p weights +1 and q weights -1 is constant unless
88
- # -p <= b <= q - 1; those biases lie inside the swept range.
89
- nonconst = sum(sum(1 for x in w if x) for w in itertools.product((-1, 0, 1), repeat=n))
90
- print(f" of these, {nonconst:,} are not constant, and none of them computes "
91
- f"[r <= 0]")
92
- out["nonconstant_ternary_units"] = nonconst
93
-
94
- # The separation begins at three bits: on two bits the predicate is ternary.
95
- firstfail = None
96
- widths = {}
97
- for m in range(2, 9):
98
- f, _ = ternary_units_computing(m, predicate(m), range(-(m + 2), m + 3))
99
- widths[m] = len(f)
100
- if not f and firstfail is None:
101
- firstfail = m
102
- print(f" ternary realizations by width: {widths}; the predicate stops "
103
- f"being a ternary threshold function at {firstfail} bits")
104
- out["ternary_realizations_by_width"] = widths
105
- out["first_width_without_ternary_unit"] = firstfail
106
- ok &= firstfail == 3
107
-
108
- # Depth two suffices, by the cell the host uses.
109
- z = (X.sum(dim=1) == 0).long()
110
- leq = ((X[:, n - 1] + z) >= 1).long()
111
- two = bool((leq == target).all())
112
- print(f" the depth-two cell OR(r_7, z(r)) of the host computes it on all "
113
- f"{1 << n} results: {'yes' if two else 'NO'}")
114
- ok &= two
115
- out["depth_two_cell_exact"] = two
116
-
117
- # The 256-way address decoder is not an instance of the same exchange: it
118
- # is one ternary unit already.
119
- from matrix8 import Net
120
- net = Net()
121
- bits = [f"a{i}" for i in range(8)]
122
- net.DECODE("sel", bits, 0xA7)
123
- ins, bias = net.gates["sel"]
124
- dec_ternary = all(w in (-1, 0, 1) for _, w in ins)
125
- A = bitmatrix(8)
126
- # DECODE reads its bits most significant first
127
- w = torch.tensor([wt for _, wt in ins], dtype=torch.long)
128
- pre = A.flip(1) @ w + bias
129
- hit = (pre >= 0).long()
130
- dec_exact = bool((hit == (torch.arange(256) == 0xA7).long()).all())
131
- print(f" the address decoder of the memory is one ternary unit of fan-in "
132
- f"eight and fires on exactly one address: "
133
- f"{'yes' if dec_ternary and dec_exact else 'NO'}")
134
- ok &= dec_ternary and dec_exact
135
- out["decoder_is_one_ternary_unit"] = dec_ternary and dec_exact
136
-
137
- json.dump(out, open(runs_path("paper_ternarity.json"), "w"), indent=1)
138
- return 0 if ok else 1
139
-
140
-
141
- if __name__ == "__main__":
142
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/constructor8.py DELETED
@@ -1,580 +0,0 @@
1
- """neural_subleq8io + the universal constructor.
2
-
3
- The host is the family's one-instruction machine extended with three
4
- memory-mapped device cells (all device logic lives in the runtimes, exactly
5
- like neural_rv32's console at 0xFF00; the processor's gates are unchanged):
6
-
7
- 0xFD IN_DATA reads as the current tape byte (device-refreshed)
8
- 0xFC IN_STROBE any write advances the tape head (device clears to 0)
9
- 0xFE OUT any write emits the written byte (device clears to 0)
10
-
11
- A 21-instruction SUBLEQ program — the constructor — interprets a construction
12
- recipe streamed on the tape:
13
-
14
- tag 0 END
15
- tag 1..127 literal record: the next `tag` tape bytes, each stored
16
- negated mod 256, are emitted one per SUBLEQ instruction
17
- (M[0xFE] = 0 - M[0xFD] recovers the byte)
18
- tag 129..255 repeat record: the next tape byte (negated value) is
19
- emitted 256-tag times without strobing (tag 128 unused)
20
-
21
- `describe(file_bytes)` compiles any safetensors file in the family into a
22
- recipe; the constructor run on that recipe emits the file byte for byte, so
23
- the constructor is universal and the description selects what is built.
24
- Self-reproduction is the case where the description handed to the constructor
25
- is that of the running machine: the program's output stream is the
26
- byte-for-byte safetensors file of the host itself.
27
-
28
- The equality is machine-checked, not observed on a single run:
29
- - the recipe codec round-trips every family file (byte equality + sha256);
30
- - the constructor program is executed on three proven-equal backends
31
- (pure-integer reference, the gate-graph SubleqThresholdCPU walking the
32
- shipped netlist, and the host compiled to recurrent ternary matrices with
33
- matrix8's compiler) with cycle-lockstep spot checks between them;
34
- - the full self-reproduction runs at threshold fidelity on the matrix
35
- backend, and the emitted stream must equal the host's file exactly;
36
- - the offspring file then boots as generation 2 and reproduces again.
37
-
38
- Usage:
39
- python constructor8.py build # emit variants/neural_subleq8io.safetensors
40
- python constructor8.py verify # host soundness + codec + constructor runs
41
- python constructor8.py self # full self-reproduction at threshold fidelity
42
- python constructor8.py all
43
- """
44
-
45
- from __future__ import annotations
46
-
47
- import argparse
48
- import hashlib
49
- import json
50
- import os
51
- import sys
52
- import time
53
- from typing import Dict, List, Optional, Tuple
54
-
55
- import torch
56
- from safetensors import safe_open
57
- from safetensors.torch import save_file
58
-
59
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
60
-
61
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # repo root; this module lives in src/
62
- MODEL_PATH = os.path.join(REPO, "variants", "neural_subleq8io.safetensors")
63
-
64
- from matrix8 import Net, compile_net # machine-1 compiler, reused verbatim
65
-
66
- IN_DATA, IN_STROBE, OUT = 0xFD, 0xFC, 0xFE
67
- HALT_PC = 0xFF
68
-
69
-
70
- # =============================================================================
71
- # Build the host file (family per-gate style, .inputs wiring, ternary strict)
72
- # =============================================================================
73
-
74
- def build_host(save: bool = True) -> str:
75
- import build as B
76
- from quantize import quantize_tensors
77
-
78
- tensors: Dict[str, torch.Tensor] = {}
79
- B.add_decoder(tensors, 8, 256)
80
- B.add_memory_read_mux(tensors, 256)
81
- B.add_memory_write_cells(tensors, 256)
82
- B.add_subleq_core(tensors)
83
- tensors["manifest.data_bits"] = torch.tensor([8.0])
84
- tensors["manifest.addr_bits"] = torch.tensor([8.0])
85
- tensors["manifest.memory_bytes"] = torch.tensor([256.0])
86
- tensors["manifest.pc_width"] = torch.tensor([8.0])
87
- tensors["manifest.version"] = torch.tensor([4.0])
88
- tensors, reg, stats = B.build_inputs(tensors)
89
- tensors, counts, _, _ = quantize_tensors(tensors, ternary=False)
90
- for k, t in tensors.items():
91
- if k.endswith(".weight") and not k.startswith("manifest."):
92
- tf = t.float() if t.dtype.is_floating_point else t.to(torch.float32)
93
- assert (tf.abs() <= 1).all(), f"non-ternary weight {k}"
94
- meta = {
95
- "machine": "subleq8io",
96
- "weight_quantization": "ternary",
97
- "signal_registry": reg.to_metadata(),
98
- "io": json.dumps({"in_data": IN_DATA, "in_strobe": IN_STROBE,
99
- "out": OUT, "halt_pc": HALT_PC}),
100
- }
101
- if save:
102
- save_file(tensors, MODEL_PATH, metadata=meta)
103
- sz = os.path.getsize(MODEL_PATH)
104
- print(f" built {MODEL_PATH}: {stats['added']} wired gates, "
105
- f"{len(tensors)} tensors, {sz:,} bytes")
106
- return MODEL_PATH
107
-
108
-
109
- # =============================================================================
110
- # Recipe codec (the construction-description language)
111
- # =============================================================================
112
-
113
- def describe(data: bytes) -> bytes:
114
- """Compile raw bytes into a construction recipe for the constructor."""
115
- tape = bytearray()
116
- i, n = 0, len(data)
117
- while i < n:
118
- j = i
119
- while j < n and data[j] == data[i] and j - i < 127:
120
- j += 1
121
- if j - i >= 4:
122
- tape.append(256 - (j - i)) # repeat tag 129..252
123
- tape.append((256 - data[i]) % 256) # value, negated
124
- i = j
125
- continue
126
- k = i
127
- while k < n and k - i < 127: # literal until a run starts
128
- m = k
129
- while m < n and data[m] == data[k] and m - k < 4:
130
- m += 1
131
- if m - k >= 4:
132
- break
133
- k += 1
134
- k = max(k, i + 1)
135
- tape.append(k - i) # literal tag 1..127
136
- tape.extend((256 - x) % 256 for x in data[i:k])
137
- i = k
138
- tape.append(0) # END
139
- return bytes(tape)
140
-
141
-
142
- def decode(tape: bytes) -> bytes:
143
- """Pure-Python reference semantics of the recipe language."""
144
- out = bytearray()
145
- i = 0
146
- while True:
147
- t = tape[i]
148
- i += 1
149
- if t == 0:
150
- return bytes(out)
151
- if t <= 127:
152
- for _ in range(t):
153
- out.append((256 - tape[i]) % 256)
154
- i += 1
155
- else:
156
- out.extend([(256 - tape[i]) % 256] * (256 - t))
157
- i += 1
158
-
159
-
160
- # =============================================================================
161
- # The constructor program (21 SUBLEQ instructions + 4 variable cells)
162
- # =============================================================================
163
-
164
- Z, ONE, T1, T2 = 0xF0, 0xF1, 0xF2, 0xF3
165
-
166
- CONSTRUCTOR = [
167
- # main loop: read tag T; T1 = -T (== run length for repeat tags), T2 = T
168
- (T2, T2, 0x03), # 00 T2 = 0
169
- (T1, T1, 0x06), # 03 T1 = 0
170
- (IN_DATA, T1, 0x09), # 06 T1 = -T
171
- (Z, IN_STROBE, 0x0C), # 09 consume tag byte
172
- (T1, T2, 0x0F), # 0C T2 = T
173
- (Z, T2, 0x15), # 0F if T == 0 or T >= 128 -> 15, else literal
174
- (Z, Z, 0x24), # 12 goto LITERAL
175
- (Z, T1, 0x1B), # 15 T1 = -T: leq only when T == 0 -> END
176
- (Z, Z, 0x30), # 18 goto REPEAT (T1 holds 256-T = run length)
177
- (Z, Z, HALT_PC), # 1B END: halt
178
- (Z, Z, 0x21), # 1E (pad)
179
- (Z, Z, 0x24), # 21 (pad)
180
- (IN_DATA, OUT, 0x27), # 24 LITERAL: emit 0 - tape_byte
181
- (Z, IN_STROBE, 0x2A), # 27 consume it
182
- (ONE, T2, 0x00), # 2A T2 -= 1; done -> main loop
183
- (Z, Z, 0x24), # 2D next literal byte
184
- (IN_DATA, OUT, 0x33), # 30 REPEAT: emit 0 - value (no strobe)
185
- (ONE, T1, 0x39), # 33 T1 -= 1; done -> 39
186
- (Z, Z, 0x30), # 36 emit again
187
- (Z, IN_STROBE, 0x3C), # 39 consume the value byte
188
- (Z, Z, 0x00), # 3C goto main loop
189
- ]
190
-
191
-
192
- def constructor_image() -> List[int]:
193
- mem = [0] * 256
194
- for idx, (a, b, c) in enumerate(CONSTRUCTOR):
195
- mem[idx * 3] = a
196
- mem[idx * 3 + 1] = b
197
- mem[idx * 3 + 2] = c
198
- mem[ONE] = 1
199
- return mem
200
-
201
-
202
- # =============================================================================
203
- # Backend 1: fast pure-integer reference with devices
204
- # =============================================================================
205
-
206
- def ref_construct(tape: bytes, max_steps: int = 1 << 34,
207
- ) -> Tuple[bytes, int]:
208
- mem = constructor_image()
209
- mem[IN_DATA] = tape[0] if tape else 0
210
- out = bytearray()
211
- head = 0
212
- pc = 0
213
- steps = 0
214
- tn = len(tape)
215
- while pc != HALT_PC and steps < max_steps:
216
- A = mem[pc]
217
- Bc = mem[(pc + 1) & 0xFF]
218
- C = mem[(pc + 2) & 0xFF]
219
- r = (mem[Bc] - mem[A]) & 0xFF
220
- mem[Bc] = r
221
- pc = C if (r == 0 or r >= 0x80) else (pc + 3) & 0xFF
222
- if Bc == OUT:
223
- out.append(r)
224
- mem[OUT] = 0
225
- elif Bc == IN_STROBE:
226
- head += 1
227
- mem[IN_STROBE] = 0
228
- mem[IN_DATA] = tape[head] if head < tn else 0
229
- steps += 1
230
- return bytes(out), steps
231
-
232
-
233
- # =============================================================================
234
- # Backend 2: the gate-graph processor (shipped netlist) with devices
235
- # =============================================================================
236
-
237
- class GateHost:
238
- def __init__(self, path: str = MODEL_PATH):
239
- from machines import SubleqThresholdCPU, load_tensors
240
- from eval import load_metadata
241
- T = load_tensors(path)
242
- reg = load_metadata(path)["signal_registry"]
243
- self.cpu = SubleqThresholdCPU(T, reg)
244
-
245
- def run(self, tape: bytes, max_steps: int) -> Tuple[bytes, int, dict]:
246
- s = {"pc": 0, "mem": constructor_image(), "halted": False}
247
- s["mem"][IN_DATA] = tape[0] if tape else 0
248
- out = bytearray()
249
- head = 0
250
- n = 0
251
- while not s["halted"] and n < max_steps:
252
- Bc = s["mem"][(s["pc"] + 1) & 0xFF]
253
- s = self.cpu.step(s)
254
- if Bc == OUT:
255
- out.append(s["mem"][OUT])
256
- s["mem"][OUT] = 0
257
- elif Bc == IN_STROBE:
258
- head += 1
259
- s["mem"][IN_STROBE] = 0
260
- s["mem"][IN_DATA] = tape[head] if head < len(tape) else 0
261
- n += 1
262
- return bytes(out), n, s
263
-
264
-
265
- # =============================================================================
266
- # Backend 3: the host compiled to recurrent ternary matrices (machine 1 tech)
267
- # =============================================================================
268
-
269
- from host_netlist import subleq_state_names, build_subleq_step_net
270
-
271
-
272
- class MatrixHost:
273
- """The subleq8io processor as a recurrent ternary linear-threshold
274
- network, with the device cells poked between recurrences."""
275
-
276
- N = 8 + 1 + 2048
277
-
278
- def __init__(self, device: str = "cpu"):
279
- t0 = time.perf_counter()
280
- net, inputs, outputs = build_subleq_step_net()
281
- layers, info = compile_net(net, inputs, outputs)
282
- for W, Bt in layers:
283
- vals = set(torch.unique(W).tolist())
284
- assert vals <= {-1, 0, 1}
285
- self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
286
- self.B = [Bt.to(device=device, dtype=torch.float32) for _, Bt in layers]
287
- self.device = device
288
- self.info = info
289
- print(f" host compiled to {info['layers']} ternary matrices "
290
- f"(max width {info['max_width']}, "
291
- f"{info['total_weights']:,} weights) in "
292
- f"{time.perf_counter() - t0:.1f}s on {device}")
293
-
294
- def _vec(self, pc: int, mem: List[int], halt: bool = False) -> torch.Tensor:
295
- v = torch.zeros(self.N)
296
- for k in range(8):
297
- v[k] = (pc >> (7 - k)) & 1
298
- v[8] = 1.0 if halt else 0.0
299
- for j in range(256):
300
- for k in range(8):
301
- v[9 + j * 8 + k] = (mem[j] >> (7 - k)) & 1
302
- return v
303
-
304
- def step(self, v: torch.Tensor) -> torch.Tensor:
305
- for W, b in zip(self.W, self.B):
306
- v = ((v @ W.T + b) >= 0).float()
307
- return v
308
-
309
- @staticmethod
310
- def _byte(vcpu: torch.Tensor, j: int) -> int:
311
- x = 0
312
- for k in range(8):
313
- x = (x << 1) | int(vcpu[9 + j * 8 + k])
314
- return x
315
-
316
- def run(self, tape: bytes, max_steps: int, expect: Optional[bytes] = None,
317
- report_every: int = 100000) -> Tuple[bytes, int]:
318
- mem = constructor_image()
319
- mem[IN_DATA] = tape[0] if tape else 0
320
- v = self._vec(0, mem).unsqueeze(0).to(self.device)
321
- out = bytearray()
322
- head = 0
323
- n = 0
324
- idx_pch = torch.arange(0, 9, device=self.device)
325
- t0 = time.perf_counter()
326
- while n < max_steps:
327
- front = v[0, idx_pch].tolist()
328
- if front[8] >= 0.5:
329
- break
330
- pcv = 0
331
- for k in range(8):
332
- pcv = (pcv << 1) | int(front[k])
333
- bidx = (pcv + 1) & 0xFF
334
- bbits = v[0, 9 + bidx * 8: 9 + bidx * 8 + 8].tolist()
335
- Bc = 0
336
- for k in range(8):
337
- Bc = (Bc << 1) | int(bbits[k])
338
- v = self.step(v)
339
- n += 1
340
- if Bc == OUT:
341
- ob = v[0, 9 + OUT * 8: 9 + OUT * 8 + 8].tolist()
342
- val = 0
343
- for k in range(8):
344
- val = (val << 1) | int(ob[k])
345
- out.append(val)
346
- if expect is not None and out[-1] != expect[len(out) - 1]:
347
- raise AssertionError(
348
- f"stream diverged at byte {len(out) - 1}: "
349
- f"got {out[-1]:#04x} want {expect[len(out) - 1]:#04x}")
350
- v[0, 9 + OUT * 8: 9 + OUT * 8 + 8] = 0.0
351
- elif Bc == IN_STROBE:
352
- head += 1
353
- v[0, 9 + IN_STROBE * 8: 9 + IN_STROBE * 8 + 8] = 0.0
354
- nxt = tape[head] if head < len(tape) else 0
355
- for k in range(8):
356
- v[0, 9 + IN_DATA * 8 + k] = float((nxt >> (7 - k)) & 1)
357
- if report_every and n % report_every == 0:
358
- rate = n / (time.perf_counter() - t0)
359
- print(f" ... {n:,} recurrences, {len(out):,} bytes emitted "
360
- f"({rate:,.0f} instr/s)")
361
- return bytes(out), n
362
-
363
- def exhaustive_datapath(self) -> bool:
364
- """All 65,536 (x, y) operand pairs through the matrices in one batch."""
365
- xs = torch.arange(65536) % 256
366
- ys = torch.arange(65536) // 256
367
- V = torch.zeros(65536, self.N)
368
- mem = [0] * 256
369
- mem[0], mem[1], mem[2] = 0x90, 0x91, 0x30
370
- v0 = self._vec(0, mem)
371
- V[:] = v0
372
- for k in range(8):
373
- V[:, 9 + 0x90 * 8 + k] = ((xs >> (7 - k)) & 1).float()
374
- V[:, 9 + 0x91 * 8 + k] = ((ys >> (7 - k)) & 1).float()
375
- V = V.to(self.device)
376
- for W, b in zip(self.W, self.B):
377
- V = ((V @ W.T + b) >= 0).float()
378
- V = V.cpu()
379
- r = torch.zeros(65536, dtype=torch.long)
380
- for k in range(8):
381
- r = (r << 1) | V[:, 9 + 0x91 * 8 + k].long()
382
- pcv = torch.zeros(65536, dtype=torch.long)
383
- for k in range(8):
384
- pcv = (pcv << 1) | V[:, k].long()
385
- exp_r = (ys - xs) & 0xFF
386
- exp_leq = (exp_r == 0) | (exp_r >= 0x80)
387
- exp_pc = torch.where(exp_leq, torch.full_like(pcv, 0x30),
388
- torch.full_like(pcv, 3))
389
- ok = bool((r == exp_r).all()) and bool((pcv == exp_pc).all())
390
- print(f" {'OK ' if ok else 'FAIL'} matrix datapath exhaustive: "
391
- f"65,536/65,536 subtract results and branch decisions exact")
392
- return ok
393
-
394
-
395
- # =============================================================================
396
- # Verification
397
- # =============================================================================
398
-
399
- def sha(b: bytes) -> str:
400
- return hashlib.sha256(b).hexdigest()[:16]
401
-
402
-
403
- def family_files() -> List[str]:
404
- files = [os.path.join(REPO, "neural_computer.safetensors")]
405
- vdir = os.path.join(REPO, "variants")
406
- files += sorted(os.path.join(vdir, f) for f in os.listdir(vdir)
407
- if f.endswith(".safetensors"))
408
- return files
409
-
410
-
411
- def verify(device: str) -> bool:
412
- ok = True
413
-
414
- print("\n[1/5] Host machine soundness (shipped netlist, exhaustive datapath)")
415
- from eval import NetlistEvaluator, load_metadata
416
- from machines import load_tensors
417
- T = load_tensors(MODEL_PATH)
418
- reg = load_metadata(MODEL_PATH)["signal_registry"]
419
- ne = NetlistEvaluator(T, reg, "subleq")
420
- a_v = torch.arange(65536) % 256
421
- b_v = torch.arange(65536) // 256
422
- ext = {}
423
- for k in range(8):
424
- ext[f"$a[{k}]"] = ((a_v >> k) & 1).float()
425
- ext[f"$b[{k}]"] = ((b_v >> k) & 1).float()
426
- ext[f"$pc[{k}]"] = torch.zeros(65536)
427
- ext[f"$c[{k}]"] = torch.zeros(65536)
428
- o = ne.run(ext)
429
- rr = sum(o[f"subleq.sub.fa{k}.ha2.sum.layer2"][:, 0].long() << k for k in range(8))
430
- lq = o["subleq.leq"][:, 0].long()
431
- exp_r = (b_v - a_v) & 0xFF
432
- exp_l = ((exp_r == 0) | (exp_r >= 128)).long()
433
- good = bool((rr == exp_r).all()) and bool((lq == exp_l).all())
434
- print(f" {'OK ' if good else 'FAIL'} gate netlist exhaustive: 65,536/65,536 exact")
435
- ok &= good
436
-
437
- print("\n[2/5] Recipe codec round-trip over the whole family")
438
- total = 0
439
- for p in family_files():
440
- data = open(p, "rb").read()
441
- tape = describe(data)
442
- good = decode(tape) == data
443
- ok &= good
444
- total += len(data)
445
- print(f" {'OK ' if good else 'FAIL'} {os.path.basename(p):<44} "
446
- f"{len(data):>10,} B -> tape {len(tape):>10,} B sha {sha(data)}")
447
- print(f" ({total / 1e6:.0f} MB described and decoded back, byte-identical)")
448
-
449
- print("\n[3/5] Constructor program on the integer reference "
450
- "(output stream == target file, byte for byte)")
451
- targets = [MODEL_PATH,
452
- os.path.join(REPO, "variants", "neural_subleq8.safetensors"),
453
- os.path.join(REPO, "variants", "neural_matrix8.safetensors")]
454
- for p in targets:
455
- data = open(p, "rb").read()
456
- tape = describe(data)
457
- t0 = time.perf_counter()
458
- out, steps = ref_construct(tape)
459
- dt = time.perf_counter() - t0
460
- good = out == data
461
- ok &= good
462
- tag = "SELF-REPRODUCTION" if p == MODEL_PATH else "universal build"
463
- print(f" {'OK ' if good else 'FAIL'} {os.path.basename(p):<40} "
464
- f"{len(out):>10,} B in {steps:>12,} instructions ({dt:.1f}s) "
465
- f"[{tag}] sha {sha(out)}")
466
-
467
- print("\n[4/5] Matrix backend (machine-1 compile of the host)")
468
- mh = MatrixHost(device=device)
469
- ok &= mh.exhaustive_datapath()
470
-
471
- print("\n[5/5] Backend lockstep on a shared prefix "
472
- "(gate graph vs matrices vs reference, first 600 cycles)")
473
- data = open(MODEL_PATH, "rb").read()
474
- tape = describe(data)
475
- gh = GateHost()
476
- s = {"pc": 0, "mem": constructor_image(), "halted": False}
477
- s["mem"][IN_DATA] = tape[0]
478
- mem2 = constructor_image()
479
- mem2[IN_DATA] = tape[0]
480
- v = mh._vec(0, mem2).unsqueeze(0).to(device)
481
- headg = headm = 0
482
- outg, outm = bytearray(), bytearray()
483
- good = True
484
- for n in range(600):
485
- Bg = s["mem"][(s["pc"] + 1) & 0xFF]
486
- s = gh.cpu.step(s)
487
- if Bg == OUT:
488
- outg.append(s["mem"][OUT])
489
- s["mem"][OUT] = 0
490
- elif Bg == IN_STROBE:
491
- headg += 1
492
- s["mem"][IN_STROBE] = 0
493
- s["mem"][IN_DATA] = tape[headg] if headg < len(tape) else 0
494
- vc = v[0].cpu()
495
- pcm = 0
496
- for k in range(8):
497
- pcm = (pcm << 1) | int(vc[k])
498
- Bm = MatrixHost._byte(vc, (pcm + 1) & 0xFF)
499
- v = mh.step(v)
500
- if Bm == OUT:
501
- outm.append(MatrixHost._byte(v[0].cpu(), OUT))
502
- v[0, 9 + OUT * 8: 9 + OUT * 8 + 8] = 0.0
503
- elif Bm == IN_STROBE:
504
- headm += 1
505
- v[0, 9 + IN_STROBE * 8: 9 + IN_STROBE * 8 + 8] = 0.0
506
- nxt = tape[headm] if headm < len(tape) else 0
507
- for k in range(8):
508
- v[0, 9 + IN_DATA * 8 + k] = float((nxt >> (7 - k)) & 1)
509
- vc = v[0].cpu()
510
- pcm = 0
511
- for k in range(8):
512
- pcm = (pcm << 1) | int(vc[k])
513
- memm = [MatrixHost._byte(vc, j) for j in range(256)]
514
- if pcm != s["pc"] or memm != s["mem"] or outg != outm:
515
- print(f" FAIL lockstep diverged at cycle {n}")
516
- good = False
517
- break
518
- print(f" {'OK ' if good else 'FAIL'} gate graph and matrix form agree "
519
- f"cycle-for-cycle for 600 cycles ({len(outg)} bytes emitted identically)")
520
- ok &= good
521
-
522
- print("\nCONSTRUCTOR VERIFICATION:", "PASS" if ok else "FAIL")
523
- return ok
524
-
525
-
526
- def self_reproduce(device: str, gen2: bool = True) -> bool:
527
- """Full self-reproduction at threshold fidelity on the matrix backend."""
528
- data = open(MODEL_PATH, "rb").read()
529
- tape = describe(data)
530
- print(f"\nSelf-reproduction on the threshold matrices: target "
531
- f"{len(data):,} bytes (sha {sha(data)}), tape {len(tape):,} bytes")
532
- ref_out, ref_steps = ref_construct(tape)
533
- assert ref_out == data, "reference construction failed"
534
- print(f" reference: {ref_steps:,} instructions")
535
-
536
- mh = MatrixHost(device=device)
537
- t0 = time.perf_counter()
538
- out, n = mh.run(tape, max_steps=ref_steps + 64, expect=data)
539
- dt = time.perf_counter() - t0
540
- ok = out == data and n == ref_steps
541
- print(f" GEN 1: emitted {len(out):,} bytes in {n:,} recurrences "
542
- f"({dt / 60:.1f} min, {n / dt:,.0f} instr/s) sha {sha(out)}")
543
- print(f" {'OK ' if ok else 'FAIL'} output stream == host safetensors file, "
544
- f"byte for byte; instruction count matches reference exactly")
545
-
546
- if ok and gen2:
547
- off_path = os.path.join(REPO, "variants", "_offspring_subleq8io.safetensors")
548
- with open(off_path, "wb") as f:
549
- f.write(out)
550
- gh = GateHost(off_path) # boot the offspring's own gates
551
- probe_tape = describe(out[:2048])
552
- want = out[:2048]
553
- got, steps, _ = gh.run(probe_tape, max_steps=len(probe_tape) * 8 + 64)
554
- good2 = got == want
555
- print(f" GEN 2: offspring file booted as a machine (gate graph); "
556
- f"constructed the first {len(want):,} bytes of itself in "
557
- f"{steps:,} instructions: {'exact' if good2 else 'MISMATCH'}")
558
- os.remove(off_path)
559
- ok &= good2
560
- print("SELF-REPRODUCTION:", "PASS" if ok else "FAIL")
561
- return ok
562
-
563
-
564
- def main() -> int:
565
- ap = argparse.ArgumentParser()
566
- ap.add_argument("cmd", choices=["build", "verify", "self", "all"])
567
- ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
568
- args = ap.parse_args()
569
- rc = 0
570
- if args.cmd in ("build", "all"):
571
- build_host(save=True)
572
- if args.cmd in ("verify", "all"):
573
- rc |= 0 if verify(args.device) else 1
574
- if args.cmd in ("self", "all"):
575
- rc |= 0 if self_reproduce(args.device) else 1
576
- return rc
577
-
578
-
579
- if __name__ == "__main__":
580
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/eval.py CHANGED
@@ -1,30 +1,15 @@
1
- """
2
- Unified Evaluation Suite for 8-bit Threshold Computer
3
- ======================================================
4
- GPU-batched evaluation with per-circuit reporting.
5
- Includes CPU runtime for threshold-weight execution.
6
-
7
- Usage:
8
- python eval.py # Run circuit evaluation
9
- python eval.py --device cpu # CPU mode
10
- python eval.py --pop_size 1000 # Population mode for evolution
11
- python eval.py --cpu-test # Run CPU smoke test
12
-
13
- The single gate-routed CPU runtime is GenericThresholdCPU in eval_all.py
14
- (manifest-sized); the pure-Python reference (CPUState / ref_step) lives here
15
- for cross-checks.
16
-
17
- API (for prune_weights.py):
18
- from eval import load_model, create_population, BatchedFitnessEvaluator
19
- """
20
 
21
  import argparse
22
  import json
23
  import os
 
24
  import time
25
  from collections import defaultdict
26
  from dataclasses import dataclass, field
27
- from typing import Callable, Dict, List, Optional, Tuple
 
28
 
29
  import torch
30
  from safetensors import safe_open
@@ -71,11 +56,7 @@ def load_metadata(path: str = MODEL_PATH) -> Dict:
71
 
72
 
73
  def get_manifest(tensors: Dict[str, torch.Tensor]) -> Dict[str, int]:
74
- """Extract manifest values from tensors.
75
-
76
- Returns dict with data_bits, addr_bits, memory_bytes, version.
77
- Defaults to 8-bit data, 16-bit addr for legacy models.
78
- """
79
  return {
80
  'data_bits': int(tensors.get('manifest.data_bits', torch.tensor([8.0])).item()),
81
  'addr_bits': int(tensors.get('manifest.addr_bits',
@@ -102,16 +83,25 @@ def create_population(
102
  # =============================================================================
103
 
104
  FLAG_NAMES = ["Z", "N", "C", "V"]
 
105
  CTRL_NAMES = ["HALT", "MEM_WE", "MEM_RE", "RESERVED"]
106
 
107
  PC_BITS = 16
 
108
  IR_BITS = 16
 
109
  REG_BITS = 8
 
110
  REG_COUNT = 4
 
111
  FLAG_BITS = 4
 
112
  SP_BITS = 16
 
113
  CTRL_BITS = 4
 
114
  MEM_BYTES = 65536
 
115
  MEM_BITS = MEM_BYTES * 8
116
 
117
  STATE_BITS = PC_BITS + IR_BITS + (REG_BITS * REG_COUNT) + FLAG_BITS + SP_BITS + CTRL_BITS + MEM_BITS
@@ -405,18 +395,9 @@ def _fill_smoke_program(mem: List[int]) -> None:
405
 
406
 
407
  def run_smoke_test() -> int:
408
- """Smoke test through the single gate-routed CPU runtime
409
- (GenericThresholdCPU), cross-checked against the pure-Python reference:
410
-
411
- 1. LOAD 5, LOAD 7, ADD, STORE -> MEM[0x0102] = 12
412
- 2. LOAD 2, LOAD 131, MUL, STORE -> MEM[0x0105] = (2*131) & 0xFF = 6
413
- 3. Countdown loop 3+2+1 via SUB/JNZ -> MEM[0x0109] = 6
414
-
415
- Runs on the 1 KB variant so the threshold pass finishes in seconds; the
416
- reference runs at 64 KB in pure Python.
417
- """
418
  import os
419
- from eval_all import GenericThresholdCPU # lazy: avoids eval<->eval_all cycle
420
 
421
  expected = {0x0102: 12, 0x0105: 6, 0x0109: 6}
422
 
@@ -461,18 +442,7 @@ def run_smoke_test() -> int:
461
  # =============================================================================
462
 
463
  class NetlistEvaluator:
464
- """Evaluate a self-contained circuit from its shipped wiring metadata.
465
-
466
- The gate graph is built from the .inputs tensors and the header signal
467
- registry — the same artifacts safetensors2verilog consumes — so a test
468
- driven through this class proves the file itself encodes the circuit,
469
- with no wiring knowledge living in Python.
470
-
471
- External signals are names starting with '$' (plus the constants #0/#1);
472
- the caller binds them per evaluation. Gate outputs are published under
473
- their full gate names. Evaluation is batched over test vectors and,
474
- for population dicts, over population slots.
475
- """
476
 
477
  def __init__(self, tensors: Dict[str, torch.Tensor],
478
  signal_registry: Dict[str, str], prefix: str,
@@ -578,8 +548,7 @@ class NetlistEvaluator:
578
  return sorted(out)
579
 
580
  def run(self, external: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
581
- """Evaluate the circuit. Each external is a [num_tests] tensor (or a
582
- scalar); every returned gate output is [num_tests, pop_size]."""
583
  first = next(iter(external.values()))
584
  if not torch.is_tensor(first):
585
  num_tests = 1
@@ -635,8 +604,7 @@ class _SlotView:
635
 
636
 
637
  def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float:
638
- """Decode an IEEE 754 word to a Python float. float64 represents every
639
- float16/float32 value exactly, so the oracle comparisons are exact."""
640
  sign = (word >> (exp_bits + frac_bits)) & 1
641
  exp = (word >> frac_bits) & ((1 << exp_bits) - 1)
642
  frac = word & ((1 << frac_bits) - 1)
@@ -653,8 +621,7 @@ def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float:
653
 
654
 
655
  def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int]]:
656
- """Directed IEEE edge encodings (every category, both signs) plus seeded
657
- random words for a family."""
658
  import random
659
  E, F = exp_bits, frac_bits
660
  emax = (1 << E) - 1
@@ -684,10 +651,7 @@ def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int
684
 
685
 
686
  def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
687
- """Expected product word under the documented contract: exact IEEE
688
- specials (NaN, infinities, signed zeros), subnormal operands and
689
- gradual-underflow subnormal results, round-to-nearest-even mantissa.
690
- Pure integer arithmetic, so exact."""
691
  E, F = exp_bits, frac_bits
692
  emax = (1 << E) - 1
693
  fmask = (1 << F) - 1
@@ -752,10 +716,7 @@ def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
752
 
753
 
754
  def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
755
- """Expected quotient word under the documented contract: exact IEEE
756
- specials (NaN, infinities, x/0 -> inf, 0/0 and inf/inf -> NaN, signed
757
- zeros), subnormal operands and gradual-underflow subnormal results,
758
- round-to-nearest-even mantissa."""
759
  E, F = exp_bits, frac_bits
760
  emax = (1 << E) - 1
761
  fmask = (1 << F) - 1
@@ -817,10 +778,7 @@ def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
817
 
818
 
819
  def float_fma_oracle(aw: int, bw: int, cw: int, exp_bits: int, frac_bits: int) -> int:
820
- """Expected word for round(a*b + c) with a SINGLE rounding: exact IEEE
821
- specials (NaN, infinities, inf*0 and inf-inf -> NaN, signed zeros, exact
822
- cancellation -> +0), subnormal operands and gradual-underflow results,
823
- round-to-nearest-even. Pure integer arithmetic, so exact."""
824
  E, F = exp_bits, frac_bits
825
  emax = (1 << E) - 1
826
  fmask = (1 << F) - 1
@@ -910,11 +868,7 @@ def float_fma_oracle(aw: int, bw: int, cw: int, exp_bits: int, frac_bits: int) -
910
 
911
 
912
  def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
913
- """Expected sum word under the documented contract: exact IEEE specials
914
- (NaN, infinities, opposite-sign infinities -> NaN, signed zeros, exact
915
- cancellation -> +0), subnormal operands and gradual-underflow subnormal
916
- results, round-to-nearest-even mantissa. A zero operand passes the other
917
- through verbatim. Pure integer arithmetic, so exact."""
918
  E, F = exp_bits, frac_bits
919
  emax = (1 << E) - 1
920
  fmask = (1 << F) - 1
@@ -998,10 +952,7 @@ def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
998
  # =============================================================================
999
 
1000
  class BatchedFitnessEvaluator:
1001
- """
1002
- GPU-batched fitness evaluator with per-circuit reporting.
1003
- Tests all circuits comprehensively.
1004
- """
1005
 
1006
  def __init__(self, device: str = 'cuda', model_path: str = MODEL_PATH, tensors: Dict[str, torch.Tensor] = None):
1007
  self.device = device
@@ -1263,14 +1214,7 @@ class BatchedFitnessEvaluator:
1263
  # =========================================================================
1264
 
1265
  def _eval_xor(self, pop: Dict, prefix: str, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
1266
- """Evaluate XOR gate with or/nand decomposition.
1267
-
1268
- Args:
1269
- a, b: Tensors of shape [num_tests] or [num_tests, pop_size]
1270
-
1271
- Returns:
1272
- Tensor of shape [num_tests, pop_size]
1273
- """
1274
  pop_size = next(iter(pop.values())).shape[0]
1275
 
1276
  # Ensure inputs are [num_tests, pop_size]
@@ -1300,14 +1244,7 @@ class BatchedFitnessEvaluator:
1300
 
1301
  def _eval_single_fa(self, pop: Dict, prefix: str,
1302
  a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
1303
- """Evaluate single full adder.
1304
-
1305
- Args:
1306
- a, b, cin: Tensors of shape [num_tests] or [num_tests, pop_size]
1307
-
1308
- Returns:
1309
- sum_out, cout: Both of shape [num_tests, pop_size]
1310
- """
1311
  pop_size = next(iter(pop.values())).shape[0]
1312
 
1313
  # Ensure inputs are [num_tests, pop_size]
@@ -1768,11 +1705,7 @@ class BatchedFitnessEvaluator:
1768
  a_bits_2d: torch.Tensor,
1769
  b_bits_2d: torch.Tensor,
1770
  ) -> Dict[str, torch.Tensor]:
1771
- """Walk the ternary bit-cascade comparator generated by
1772
- build.add_bit_cascade_compare. Returns a dict with gt/lt/ge/le/eq each of
1773
- shape [num_tests, pop_size]. a_bits_2d, b_bits_2d are [num_tests, bits]
1774
- MSB-first.
1775
- """
1776
  pop_size = next(iter(pop.values())).shape[0]
1777
 
1778
  # Per-bit gt, lt, eq
@@ -2469,12 +2402,7 @@ class BatchedFitnessEvaluator:
2469
  # =========================================================================
2470
 
2471
  def _test_modular(self, pop: Dict, mod: int, debug: bool) -> Tuple[torch.Tensor, int]:
2472
- """Test modular divisibility circuit.
2473
-
2474
- Two structures: mod 3/5/6/7/9/10/11/12 use bit-cascade equality
2475
- per multiple of N (`{prefix}.eq.k{k}.*` + final OR at `{prefix}`).
2476
- mod 2/4/8 use a single-layer ternary detector at `{prefix}` directly.
2477
- """
2478
  pop_size = next(iter(pop.values())).shape[0]
2479
  prefix = f'modular.mod{mod}'
2480
 
@@ -3041,15 +2969,7 @@ class BatchedFitnessEvaluator:
3041
  return scores, total
3042
 
3043
  def _test_priority_encoder_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
3044
- """Test N-bit priority encoder (find highest set bit).
3045
-
3046
- The priority encoder is a multi-layer circuit:
3047
- 1. any_higher{pos}: OR of bits 0 to pos-1 (all higher-priority positions)
3048
- 2. is_highest{0}: bit[0] directly (MSB is always highest if set)
3049
- 3. is_highest{pos}: bit[pos] AND NOT(any_higher{pos}) for pos > 0
3050
- 4. out{bit}: OR of is_highest{pos} for all pos where (pos >> bit) & 1
3051
- 5. valid: OR of all input bits
3052
- """
3053
  import math
3054
  pop_size = next(iter(pop.values())).shape[0]
3055
  scores = torch.zeros(pop_size, device=self.device)
@@ -4013,8 +3933,7 @@ class BatchedFitnessEvaluator:
4013
 
4014
  def _test_float_unpack_pack(self, pop: Dict, family: str, word_bits: int,
4015
  debug: bool) -> Tuple[torch.Tensor, int]:
4016
- """Functionally test the unpack/pack identity buffers: every bit gate
4017
- must reproduce its binary input (0 -> 0, 1 -> 1)."""
4018
  pop_size = next(iter(pop.values())).shape[0]
4019
  scores = torch.zeros(pop_size, device=self.device)
4020
  total = 0
@@ -4047,9 +3966,7 @@ class BatchedFitnessEvaluator:
4047
 
4048
  def _test_float_classify(self, pop: Dict, family: str, exp_bits: int,
4049
  frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
4050
- """Functionally test the classify subcircuit: the exponent/fraction
4051
- field predicates and the is_zero / is_subnormal / is_inf / is_nan AND
4052
- gates, against IEEE 754 categories over edge-case encodings."""
4053
  pop_size = next(iter(pop.values())).shape[0]
4054
  scores = torch.zeros(pop_size, device=self.device)
4055
  total = 0
@@ -4136,10 +4053,7 @@ class BatchedFitnessEvaluator:
4136
 
4137
  def _test_float_cmp_composed(self, pop: Dict, family: str, exp_bits: int,
4138
  frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
4139
- """Composed IEEE comparison test driven entirely by the shipped
4140
- wiring: the netlist is reconstructed from the .inputs metadata and
4141
- evaluated end to end, then checked against exact IEEE semantics
4142
- (NaN unordered, +0 == -0, subnormals ordered, mixed signs)."""
4143
  pop_size = next(iter(pop.values())).shape[0]
4144
  scores = torch.zeros(pop_size, device=self.device)
4145
  total = 0
@@ -4211,10 +4125,7 @@ class BatchedFitnessEvaluator:
4211
  def _test_float_arith_composed(self, pop: Dict, family: str, op: str,
4212
  oracle, exp_bits: int, frac_bits: int,
4213
  debug: bool) -> Tuple[torch.Tensor, int]:
4214
- """Composed arithmetic test for a self-contained float pipeline: the
4215
- netlist is rebuilt from the .inputs metadata, evaluated end to end
4216
- over IEEE edge-case and random operand pairs, and the assembled
4217
- output word is compared to the exact integer oracle."""
4218
  pop_size = next(iter(pop.values())).shape[0]
4219
  scores = torch.zeros(pop_size, device=self.device)
4220
  total = 0
@@ -4287,9 +4198,7 @@ class BatchedFitnessEvaluator:
4287
 
4288
  def _test_float_fma_composed(self, pop: Dict, family: str, exp_bits: int,
4289
  frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
4290
- """Composed fused-multiply-add test: rebuild the netlist from .inputs,
4291
- evaluate round(a*b+c) end to end over edge and random triples, compare
4292
- to the single-rounding oracle."""
4293
  pop_size = next(iter(pop.values())).shape[0]
4294
  scores = torch.zeros(pop_size, device=self.device)
4295
  prefix = f"{family}.fma"
@@ -4756,12 +4665,7 @@ class BatchedFitnessEvaluator:
4756
 
4757
  def _pop_modN(self, pop: Dict, pop_size: int, val_bits: torch.Tensor,
4758
  modulus: int) -> torch.Tensor:
4759
- """Drive the bit-cascade modular.mod{N} divisibility detector.
4760
-
4761
- Returns a (pop_size,) tensor: 1 iff the 8-bit value (MSB-first bits in
4762
- val_bits) is divisible by ``modulus``. Walks the per-multiple match
4763
- gates (modular.modN.eq.k{val}.bit{i}.match -> .all -> top-level OR).
4764
- """
4765
  ks = [k for k in range(256) if k % modulus == 0]
4766
  alls = []
4767
  for k in ks:
@@ -4782,11 +4686,7 @@ class BatchedFitnessEvaluator:
4782
  def _pop_cmp8bit(self, pop: Dict, pop_size: int,
4783
  a_bits: torch.Tensor, b_bits: torch.Tensor,
4784
  kind: str) -> torch.Tensor:
4785
- """Drive the bit-cascade comparator (cmp8bit) over a population.
4786
-
4787
- Returns a (pop_size,) tensor of heaviside outputs for the requested
4788
- comparison kind ('gt' | 'lt' | 'eq'). Bit 0 is MSB.
4789
- """
4790
  def apply(name: str, inp: torch.Tensor, fan_in: int) -> torch.Tensor:
4791
  w = pop[f'{name}.weight'].view(pop_size, fan_in)
4792
  b = pop[f'{name}.bias'].view(pop_size)
@@ -5049,16 +4949,7 @@ class BatchedFitnessEvaluator:
5049
  # =========================================================================
5050
 
5051
  def evaluate(self, population: Dict[str, torch.Tensor], debug: bool = False) -> torch.Tensor:
5052
- """
5053
- Evaluate population fitness with per-circuit reporting.
5054
-
5055
- Args:
5056
- population: Dict of tensors, each with shape [pop_size, ...]
5057
- debug: If True, print per-circuit results
5058
-
5059
- Returns:
5060
- Tensor of fitness scores [pop_size], normalized to [0, 1]
5061
- """
5062
  self.results = []
5063
  self.category_scores = {}
5064
 
@@ -5475,5 +5366,1542 @@ def main():
5475
  return 1
5476
 
5477
 
5478
- if __name__ == '__main__':
5479
- exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluates the threshold computers: the fitness suite, every variant, and the CPU programs."""
2
+ from __future__ import annotations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  import argparse
5
  import json
6
  import os
7
+ import sys
8
  import time
9
  from collections import defaultdict
10
  from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Callable, Dict, List, Optional, Tuple, Any
13
 
14
  import torch
15
  from safetensors import safe_open
 
56
 
57
 
58
  def get_manifest(tensors: Dict[str, torch.Tensor]) -> Dict[str, int]:
59
+ """Extract manifest values from tensors."""
 
 
 
 
60
  return {
61
  'data_bits': int(tensors.get('manifest.data_bits', torch.tensor([8.0])).item()),
62
  'addr_bits': int(tensors.get('manifest.addr_bits',
 
83
  # =============================================================================
84
 
85
  FLAG_NAMES = ["Z", "N", "C", "V"]
86
+
87
  CTRL_NAMES = ["HALT", "MEM_WE", "MEM_RE", "RESERVED"]
88
 
89
  PC_BITS = 16
90
+
91
  IR_BITS = 16
92
+
93
  REG_BITS = 8
94
+
95
  REG_COUNT = 4
96
+
97
  FLAG_BITS = 4
98
+
99
  SP_BITS = 16
100
+
101
  CTRL_BITS = 4
102
+
103
  MEM_BYTES = 65536
104
+
105
  MEM_BITS = MEM_BYTES * 8
106
 
107
  STATE_BITS = PC_BITS + IR_BITS + (REG_BITS * REG_COUNT) + FLAG_BITS + SP_BITS + CTRL_BITS + MEM_BITS
 
395
 
396
 
397
  def run_smoke_test() -> int:
398
+ """Programs through the gate-routed CPU runtime against the Python reference."""
 
 
 
 
 
 
 
 
 
399
  import os
400
+ pass # lazy: avoids eval<->eval_all cycle
401
 
402
  expected = {0x0102: 12, 0x0105: 6, 0x0109: 6}
403
 
 
442
  # =============================================================================
443
 
444
  class NetlistEvaluator:
445
+ """Evaluate a self-contained circuit from its shipped wiring metadata."""
 
 
 
 
 
 
 
 
 
 
 
446
 
447
  def __init__(self, tensors: Dict[str, torch.Tensor],
448
  signal_registry: Dict[str, str], prefix: str,
 
548
  return sorted(out)
549
 
550
  def run(self, external: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
551
+ """Evaluate the circuit."""
 
552
  first = next(iter(external.values()))
553
  if not torch.is_tensor(first):
554
  num_tests = 1
 
604
 
605
 
606
  def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float:
607
+ """Decode an IEEE 754 word to a Python float, exactly."""
 
608
  sign = (word >> (exp_bits + frac_bits)) & 1
609
  exp = (word >> frac_bits) & ((1 << exp_bits) - 1)
610
  frac = word & ((1 << frac_bits) - 1)
 
621
 
622
 
623
  def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int]]:
624
+ """Directed IEEE edge encodings (every category, both signs) plus seeded random words for a family."""
 
625
  import random
626
  E, F = exp_bits, frac_bits
627
  emax = (1 << E) - 1
 
651
 
652
 
653
  def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
654
+ """The expected IEEE product word: exact specials, subnormals, round to nearest even."""
 
 
 
655
  E, F = exp_bits, frac_bits
656
  emax = (1 << E) - 1
657
  fmask = (1 << F) - 1
 
716
 
717
 
718
  def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
719
+ """The expected IEEE quotient word: exact specials, subnormals, round to nearest even."""
 
 
 
720
  E, F = exp_bits, frac_bits
721
  emax = (1 << E) - 1
722
  fmask = (1 << F) - 1
 
778
 
779
 
780
  def float_fma_oracle(aw: int, bw: int, cw: int, exp_bits: int, frac_bits: int) -> int:
781
+ """The expected word of round(a*b + c), rounded once."""
 
 
 
782
  E, F = exp_bits, frac_bits
783
  emax = (1 << E) - 1
784
  fmask = (1 << F) - 1
 
868
 
869
 
870
  def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
871
+ """The expected IEEE sum word: exact specials, subnormals, round to nearest even."""
 
 
 
 
872
  E, F = exp_bits, frac_bits
873
  emax = (1 << E) - 1
874
  fmask = (1 << F) - 1
 
952
  # =============================================================================
953
 
954
  class BatchedFitnessEvaluator:
955
+ """GPU-batched fitness evaluator with per-circuit reporting."""
 
 
 
956
 
957
  def __init__(self, device: str = 'cuda', model_path: str = MODEL_PATH, tensors: Dict[str, torch.Tensor] = None):
958
  self.device = device
 
1214
  # =========================================================================
1215
 
1216
  def _eval_xor(self, pop: Dict, prefix: str, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
1217
+ """Evaluate XOR gate with or/nand decomposition."""
 
 
 
 
 
 
 
1218
  pop_size = next(iter(pop.values())).shape[0]
1219
 
1220
  # Ensure inputs are [num_tests, pop_size]
 
1244
 
1245
  def _eval_single_fa(self, pop: Dict, prefix: str,
1246
  a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
1247
+ """Evaluate single full adder."""
 
 
 
 
 
 
 
1248
  pop_size = next(iter(pop.values())).shape[0]
1249
 
1250
  # Ensure inputs are [num_tests, pop_size]
 
1705
  a_bits_2d: torch.Tensor,
1706
  b_bits_2d: torch.Tensor,
1707
  ) -> Dict[str, torch.Tensor]:
1708
+ """Walk the ternary bit-cascade comparator generated by build.add_bit_cascade_compare."""
 
 
 
 
1709
  pop_size = next(iter(pop.values())).shape[0]
1710
 
1711
  # Per-bit gt, lt, eq
 
2402
  # =========================================================================
2403
 
2404
  def _test_modular(self, pop: Dict, mod: int, debug: bool) -> Tuple[torch.Tensor, int]:
2405
+ """Test modular divisibility circuit."""
 
 
 
 
 
2406
  pop_size = next(iter(pop.values())).shape[0]
2407
  prefix = f'modular.mod{mod}'
2408
 
 
2969
  return scores, total
2970
 
2971
  def _test_priority_encoder_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
2972
+ """Test N-bit priority encoder (find highest set bit)."""
 
 
 
 
 
 
 
 
2973
  import math
2974
  pop_size = next(iter(pop.values())).shape[0]
2975
  scores = torch.zeros(pop_size, device=self.device)
 
3933
 
3934
  def _test_float_unpack_pack(self, pop: Dict, family: str, word_bits: int,
3935
  debug: bool) -> Tuple[torch.Tensor, int]:
3936
+ """Each unpack/pack identity buffer reproduces its input bit."""
 
3937
  pop_size = next(iter(pop.values())).shape[0]
3938
  scores = torch.zeros(pop_size, device=self.device)
3939
  total = 0
 
3966
 
3967
  def _test_float_classify(self, pop: Dict, family: str, exp_bits: int,
3968
  frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
3969
+ """The classify subcircuit against the IEEE 754 categories on edge encodings."""
 
 
3970
  pop_size = next(iter(pop.values())).shape[0]
3971
  scores = torch.zeros(pop_size, device=self.device)
3972
  total = 0
 
4053
 
4054
  def _test_float_cmp_composed(self, pop: Dict, family: str, exp_bits: int,
4055
  frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
4056
+ """The IEEE comparison rebuilt from the shipped wiring against exact IEEE semantics."""
 
 
 
4057
  pop_size = next(iter(pop.values())).shape[0]
4058
  scores = torch.zeros(pop_size, device=self.device)
4059
  total = 0
 
4125
  def _test_float_arith_composed(self, pop: Dict, family: str, op: str,
4126
  oracle, exp_bits: int, frac_bits: int,
4127
  debug: bool) -> Tuple[torch.Tensor, int]:
4128
+ """A float pipeline rebuilt from the shipped wiring against the exact integer oracle."""
 
 
 
4129
  pop_size = next(iter(pop.values())).shape[0]
4130
  scores = torch.zeros(pop_size, device=self.device)
4131
  total = 0
 
4198
 
4199
  def _test_float_fma_composed(self, pop: Dict, family: str, exp_bits: int,
4200
  frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
4201
+ """The fused multiply-add rebuilt from the shipped wiring against the single-rounding oracle."""
 
 
4202
  pop_size = next(iter(pop.values())).shape[0]
4203
  scores = torch.zeros(pop_size, device=self.device)
4204
  prefix = f"{family}.fma"
 
4665
 
4666
  def _pop_modN(self, pop: Dict, pop_size: int, val_bits: torch.Tensor,
4667
  modulus: int) -> torch.Tensor:
4668
+ """Drive the bit-cascade modular.mod{N} divisibility detector."""
 
 
 
 
 
4669
  ks = [k for k in range(256) if k % modulus == 0]
4670
  alls = []
4671
  for k in ks:
 
4686
  def _pop_cmp8bit(self, pop: Dict, pop_size: int,
4687
  a_bits: torch.Tensor, b_bits: torch.Tensor,
4688
  kind: str) -> torch.Tensor:
4689
+ """Drive the bit-cascade comparator (cmp8bit) over a population."""
 
 
 
 
4690
  def apply(name: str, inp: torch.Tensor, fan_in: int) -> torch.Tensor:
4691
  w = pop[f'{name}.weight'].view(pop_size, fan_in)
4692
  b = pop[f'{name}.bias'].view(pop_size)
 
4949
  # =========================================================================
4950
 
4951
  def evaluate(self, population: Dict[str, torch.Tensor], debug: bool = False) -> torch.Tensor:
4952
+ """Evaluate population fitness with per-circuit reporting."""
 
 
 
 
 
 
 
 
 
4953
  self.results = []
4954
  self.category_scores = {}
4955
 
 
5366
  return 1
5367
 
5368
 
5369
+ # ---------------------------------------------------------------------------
5370
+ # Variant-aware threshold ALU + CPU
5371
+ # ---------------------------------------------------------------------------
5372
+
5373
+ class GenericThresholdALU:
5374
+ """Variant-aware threshold ALU walking the gates of a loaded variant."""
5375
+
5376
+ def __init__(self, tensors: Dict[str, torch.Tensor], data_bits: int):
5377
+ self.T = tensors
5378
+ self.data_bits = data_bits
5379
+
5380
+ def _g(self, name, inputs):
5381
+ w = self.T[name + ".weight"].view(-1)
5382
+ b = self.T[name + ".bias"].view(-1)
5383
+ return int(heaviside((torch.tensor(inputs, dtype=torch.float32) * w).sum() + b).item())
5384
+
5385
+ def _xor_or_nand(self, prefix, inputs):
5386
+ a, b_ = inputs
5387
+ h_or = self._g(f"{prefix}.layer1.or", [a, b_])
5388
+ h_nand = self._g(f"{prefix}.layer1.nand", [a, b_])
5389
+ return self._g(f"{prefix}.layer2", [h_or, h_nand])
5390
+
5391
+ def _fa(self, prefix, a, b, cin):
5392
+ s1 = self._xor_or_nand(f"{prefix}.ha1.sum", [a, b])
5393
+ c1 = self._g(f"{prefix}.ha1.carry", [a, b])
5394
+ s2 = self._xor_or_nand(f"{prefix}.ha2.sum", [s1, cin])
5395
+ c2 = self._g(f"{prefix}.ha2.carry", [s1, cin])
5396
+ cout = self._g(f"{prefix}.carry_or", [c1, c2])
5397
+ return s2, cout
5398
+
5399
+ def add8(self, a, b):
5400
+ a_lsb = list(reversed(int_to_bits(a, 8)))
5401
+ b_lsb = list(reversed(int_to_bits(b, 8)))
5402
+ carry = 0
5403
+ s_lsb = []
5404
+ for i in range(8):
5405
+ s, carry = self._fa(f"arithmetic.ripplecarry8bit.fa{i}", a_lsb[i], b_lsb[i], carry)
5406
+ s_lsb.append(s)
5407
+ return bits_to_int(list(reversed(s_lsb))), carry
5408
+
5409
+ def sub8(self, a, b):
5410
+ a_lsb = list(reversed(int_to_bits(a, 8)))
5411
+ b_lsb = list(reversed(int_to_bits(b, 8)))
5412
+ carry = 1
5413
+ d_lsb = []
5414
+ for i in range(8):
5415
+ notb = self._g(f"arithmetic.sub8bit.notb{i}", [b_lsb[i]])
5416
+ x1 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor1", [a_lsb[i], notb])
5417
+ x2 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor2", [x1, carry])
5418
+ and1 = self._g(f"arithmetic.sub8bit.fa{i}.and1", [a_lsb[i], notb])
5419
+ and2 = self._g(f"arithmetic.sub8bit.fa{i}.and2", [x1, carry])
5420
+ carry = self._g(f"arithmetic.sub8bit.fa{i}.or_carry", [and1, and2])
5421
+ d_lsb.append(x2)
5422
+ return bits_to_int(list(reversed(d_lsb))), carry
5423
+
5424
+ _CMP_KIND = {"greaterthan": "gt", "lessthan": "lt", "eq": "eq", "equality": "eq",
5425
+ "greaterorequal": "ge", "lessorequal": "le"}
5426
+
5427
+ def _cmp_bit_cascade(self, cmp_prefix: str, finals: Dict[str, str],
5428
+ a_bits_msb: List[int], b_bits_msb: List[int], kind: str) -> int:
5429
+ """Walk an add_bit_cascade_compare structure (bit 0 is the MSB)."""
5430
+ bits = len(a_bits_msb)
5431
+ bit_gt, bit_lt, bit_eq = [], [], []
5432
+ for i in range(bits):
5433
+ ab = [a_bits_msb[i], b_bits_msb[i]]
5434
+ bit_gt.append(self._g(f"{cmp_prefix}.bit{i}.gt", ab))
5435
+ bit_lt.append(self._g(f"{cmp_prefix}.bit{i}.lt", ab))
5436
+ eq_and = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.and", ab)
5437
+ eq_nor = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.nor", ab)
5438
+ bit_eq.append(self._g(f"{cmp_prefix}.bit{i}.eq", [eq_and, eq_nor]))
5439
+ cas_gt, cas_lt = [bit_gt[0]], [bit_lt[0]]
5440
+ for i in range(1, bits):
5441
+ eq_pref = self._g(f"{cmp_prefix}.cascade.eq_prefix.bit{i}", bit_eq[:i])
5442
+ cas_gt.append(self._g(f"{cmp_prefix}.cascade.gt.bit{i}", [eq_pref, bit_gt[i]]))
5443
+ cas_lt.append(self._g(f"{cmp_prefix}.cascade.lt.bit{i}", [eq_pref, bit_lt[i]]))
5444
+ if kind == "gt":
5445
+ return self._g(finals["gt"], cas_gt)
5446
+ if kind == "lt":
5447
+ return self._g(finals["lt"], cas_lt)
5448
+ if kind == "eq":
5449
+ return self._g(finals["eq"], bit_eq)
5450
+ if kind == "ge":
5451
+ lt = self._g(finals["lt"], cas_lt)
5452
+ not_lt = self._g(finals["ge"] + ".not_lt", [lt])
5453
+ return self._g(finals["ge"], [not_lt])
5454
+ if kind == "le":
5455
+ gt = self._g(finals["gt"], cas_gt)
5456
+ not_gt = self._g(finals["le"] + ".not_gt", [gt])
5457
+ return self._g(finals["le"], [not_gt])
5458
+ raise ValueError(kind)
5459
+
5460
+ def cmp8(self, a, b, kind):
5461
+ return self.cmp_n(a, b, kind, 8)
5462
+
5463
+ def mul8(self, a, b):
5464
+ ab = int_to_bits(a, 8)
5465
+ bb = int_to_bits(b, 8)
5466
+ result = 0
5467
+ for j in range(8):
5468
+ if bb[j] == 0:
5469
+ continue
5470
+ row = 0
5471
+ for i in range(8):
5472
+ pp = self._g(f"alu.alu8bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
5473
+ row |= (pp << (7 - i))
5474
+ shift = 7 - j
5475
+ result, _ = self.add8(result & 0xFF, (row << shift) & 0xFF)
5476
+ return result & 0xFF
5477
+
5478
+ def _packed_pair_op(self, name: str, a: int, b: int) -> int:
5479
+ """8 parallel 2-input gates stored packed: weight [16], bias [8]."""
5480
+ a_bits = int_to_bits(a, 8)
5481
+ b_bits = int_to_bits(b, 8)
5482
+ w = self.T[f"{name}.weight"].view(-1)
5483
+ bias = self.T[f"{name}.bias"].view(-1)
5484
+ out = []
5485
+ for bit in range(8):
5486
+ inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
5487
+ out.append(int(heaviside((inp * w[bit * 2:bit * 2 + 2]).sum() + bias[bit]).item()))
5488
+ return bits_to_int(out)
5489
+
5490
+ def and8(self, a, b):
5491
+ return self._packed_pair_op("alu.alu8bit.and", a, b)
5492
+
5493
+ def or8(self, a, b):
5494
+ return self._packed_pair_op("alu.alu8bit.or", a, b)
5495
+
5496
+ def xor8(self, a, b):
5497
+ a_bits = int_to_bits(a, 8)
5498
+ b_bits = int_to_bits(b, 8)
5499
+ w_or = self.T["alu.alu8bit.xor.layer1.or.weight"].view(-1)
5500
+ b_or = self.T["alu.alu8bit.xor.layer1.or.bias"].view(-1)
5501
+ w_nand = self.T["alu.alu8bit.xor.layer1.nand.weight"].view(-1)
5502
+ b_nand = self.T["alu.alu8bit.xor.layer1.nand.bias"].view(-1)
5503
+ w2 = self.T["alu.alu8bit.xor.layer2.weight"].view(-1)
5504
+ b2 = self.T["alu.alu8bit.xor.layer2.bias"].view(-1)
5505
+ out = []
5506
+ for bit in range(8):
5507
+ inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
5508
+ h_or = heaviside((inp * w_or[bit * 2:bit * 2 + 2]).sum() + b_or[bit])
5509
+ h_nand = heaviside((inp * w_nand[bit * 2:bit * 2 + 2]).sum() + b_nand[bit])
5510
+ hidden = torch.stack([h_or, h_nand])
5511
+ out.append(int(heaviside((hidden * w2[bit * 2:bit * 2 + 2]).sum() + b2[bit]).item()))
5512
+ return bits_to_int(out)
5513
+
5514
+ def shl8(self, a):
5515
+ a_bits = int_to_bits(a, 8)
5516
+ out = []
5517
+ for bit in range(8):
5518
+ src = float(a_bits[bit + 1]) if bit < 7 else 0.0
5519
+ out.append(self._g(f"alu.alu8bit.shl.bit{bit}", [src]))
5520
+ return bits_to_int(out)
5521
+
5522
+ def shr8(self, a):
5523
+ a_bits = int_to_bits(a, 8)
5524
+ out = []
5525
+ for bit in range(8):
5526
+ src = float(a_bits[bit - 1]) if bit > 0 else 0.0
5527
+ out.append(self._g(f"alu.alu8bit.shr.bit{bit}", [src]))
5528
+ return bits_to_int(out)
5529
+
5530
+ def div8(self, a, b):
5531
+ """8-bit restoring division through the per-stage bit-cascade GE comparators and the sub8 subtractor."""
5532
+ if b == 0:
5533
+ return 0xFF, a
5534
+ a_bits = int_to_bits(a, 8)
5535
+ div_bits = int_to_bits(b, 8)
5536
+ quotient = 0
5537
+ remainder = 0
5538
+ for stage in range(8):
5539
+ remainder = ((remainder << 1) | a_bits[stage]) & 0xFF
5540
+ prefix = f"alu.alu8bit.div.stage{stage}"
5541
+ finals = {
5542
+ "gt": f"{prefix}.cmp_bc.gt",
5543
+ "lt": f"{prefix}.cmp_bc.lt",
5544
+ "eq": f"{prefix}.cmp_bc.eq",
5545
+ "ge": f"{prefix}.cmp",
5546
+ "le": f"{prefix}.cmp_bc.le",
5547
+ }
5548
+ ge = self._cmp_bit_cascade(f"{prefix}.cmp_bc", finals,
5549
+ int_to_bits(remainder, 8), div_bits, "ge")
5550
+ if ge:
5551
+ remainder, _ = self.sub8(remainder, b)
5552
+ quotient = (quotient << 1) | 1
5553
+ else:
5554
+ quotient = quotient << 1
5555
+ return quotient & 0xFF, remainder & 0xFF
5556
+
5557
+ # ----- N-bit primitives (for 16-bit and 32-bit variants) ----------------
5558
+
5559
+ def add_n(self, a: int, b: int, bits: int):
5560
+ """Width-generic ripple-carry add via arithmetic.ripplecarry{N}bit."""
5561
+ prefix = f"arithmetic.ripplecarry{bits}bit"
5562
+ a_lsb = list(reversed(int_to_bits(a, bits)))
5563
+ b_lsb = list(reversed(int_to_bits(b, bits)))
5564
+ carry = 0
5565
+ s_lsb = []
5566
+ for i in range(bits):
5567
+ s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], b_lsb[i], carry)
5568
+ s_lsb.append(s)
5569
+ return bits_to_int(list(reversed(s_lsb))), carry
5570
+
5571
+ def sub_n(self, a: int, b: int, bits: int):
5572
+ """N-bit two's-complement subtract via arithmetic.sub{N}bit (N >= 16)."""
5573
+ prefix = f"arithmetic.sub{bits}bit"
5574
+ a_lsb = list(reversed(int_to_bits(a, bits)))
5575
+ b_lsb = list(reversed(int_to_bits(b, bits)))
5576
+ # NOT each B bit
5577
+ notb = [self._g(f"{prefix}.not_b.bit{i}", [b_lsb[i]]) for i in range(bits)]
5578
+ carry = 1 # carry-in = 1 for two's-complement
5579
+ d_lsb = []
5580
+ for i in range(bits):
5581
+ s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], notb[i], carry)
5582
+ d_lsb.append(s)
5583
+ return bits_to_int(list(reversed(d_lsb))), carry
5584
+
5585
+ def cmp_n(self, a: int, b: int, kind: str, bits: int):
5586
+ """N-bit unsigned comparison through the bit-cascade comparator."""
5587
+ short = self._CMP_KIND[kind]
5588
+ finals = {
5589
+ "gt": f"arithmetic.greaterthan{bits}bit",
5590
+ "lt": f"arithmetic.lessthan{bits}bit",
5591
+ "eq": f"arithmetic.equality{bits}bit",
5592
+ "ge": f"arithmetic.greaterorequal{bits}bit",
5593
+ "le": f"arithmetic.lessorequal{bits}bit",
5594
+ }
5595
+ return self._cmp_bit_cascade(f"arithmetic.cmp{bits}bit", finals,
5596
+ int_to_bits(a, bits), int_to_bits(b, bits), short)
5597
+
5598
+ def mul_n(self, a: int, b: int, bits: int):
5599
+ """N-bit shift-add multiply (low N bits only)."""
5600
+ ab = int_to_bits(a, bits)
5601
+ bb = int_to_bits(b, bits)
5602
+ mask = (1 << bits) - 1
5603
+ result = 0
5604
+ for j in range(bits):
5605
+ if bb[j] == 0:
5606
+ continue
5607
+ row = 0
5608
+ for i in range(bits):
5609
+ pp = self._g(f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
5610
+ row |= (pp << (bits - 1 - i))
5611
+ shift = (bits - 1) - j
5612
+ result, _ = self.add_n(result & mask, (row << shift) & mask, bits)
5613
+ return result & mask
5614
+
5615
+
5616
+ class GenericThresholdCPU:
5617
+ """Variant-aware CPU runtime. Sized from the variant's manifest."""
5618
+
5619
+ def __init__(self, tensors: Dict[str, torch.Tensor]):
5620
+ self.T = tensors
5621
+ m = get_manifest(tensors)
5622
+ self.data_bits = m["data_bits"]
5623
+ self.addr_bits = m["addr_bits"]
5624
+ self.mem_bytes = m["memory_bytes"]
5625
+ # 8-bit CPU primitives (ripplecarry8bit, sub8bit, alu.alu8bit.*, memory.*,
5626
+ # control.*) are present in every variant regardless of manifest data_bits.
5627
+ # Wider data widths simply add additional standalone ALU primitives.
5628
+ if self.mem_bytes == 0:
5629
+ raise NotImplementedError(
5630
+ "Pure-ALU variants have no memory; cannot run CPU programs"
5631
+ )
5632
+ self.alu = GenericThresholdALU(tensors, 8)
5633
+
5634
+ def _addr_decode(self, addr):
5635
+ bits = torch.tensor(int_to_bits(addr, self.addr_bits), dtype=torch.float32)
5636
+ w = self.T["memory.addr_decode.weight"]
5637
+ b = self.T["memory.addr_decode.bias"]
5638
+ return heaviside((w * bits).sum(dim=1) + b)
5639
+
5640
+ def mem_read(self, mem, addr):
5641
+ sel = self._addr_decode(addr)
5642
+ mem_bits = torch.tensor(
5643
+ [int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
5644
+ )
5645
+ and_w = self.T["memory.read.and.weight"]
5646
+ and_b = self.T["memory.read.and.bias"]
5647
+ or_w = self.T["memory.read.or.weight"]
5648
+ or_b = self.T["memory.read.or.bias"]
5649
+ out = []
5650
+ for bit in range(8):
5651
+ inp = torch.stack([mem_bits[:, bit], sel], dim=1)
5652
+ and_out = heaviside((inp * and_w[bit]).sum(dim=1) + and_b[bit])
5653
+ out.append(int(heaviside((and_out * or_w[bit]).sum() + or_b[bit]).item()))
5654
+ return bits_to_int(out)
5655
+
5656
+ def mem_write(self, mem, addr, value):
5657
+ sel = self._addr_decode(addr)
5658
+ data_bits = torch.tensor(int_to_bits(value, 8), dtype=torch.float32)
5659
+ mem_bits = torch.tensor(
5660
+ [int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
5661
+ )
5662
+ sel_w = self.T["memory.write.sel.weight"]
5663
+ sel_b = self.T["memory.write.sel.bias"]
5664
+ nsel_w = self.T["memory.write.nsel.weight"].squeeze(1)
5665
+ nsel_b = self.T["memory.write.nsel.bias"]
5666
+ and_old_w = self.T["memory.write.and_old.weight"]
5667
+ and_old_b = self.T["memory.write.and_old.bias"]
5668
+ and_new_w = self.T["memory.write.and_new.weight"]
5669
+ and_new_b = self.T["memory.write.and_new.bias"]
5670
+ or_w = self.T["memory.write.or.weight"]
5671
+ or_b = self.T["memory.write.or.bias"]
5672
+ we = torch.ones_like(sel)
5673
+ sel_inp = torch.stack([sel, we], dim=1)
5674
+ write_sel = heaviside((sel_inp * sel_w).sum(dim=1) + sel_b)
5675
+ nsel = heaviside(write_sel * nsel_w + nsel_b)
5676
+ for bit in range(8):
5677
+ old = mem_bits[:, bit]
5678
+ data_bit = data_bits[bit].expand(self.mem_bytes)
5679
+ inp_old = torch.stack([old, nsel], dim=1)
5680
+ inp_new = torch.stack([data_bit, write_sel], dim=1)
5681
+ and_old = heaviside((inp_old * and_old_w[:, bit]).sum(dim=1) + and_old_b[:, bit])
5682
+ and_new = heaviside((inp_new * and_new_w[:, bit]).sum(dim=1) + and_new_b[:, bit])
5683
+ or_inp = torch.stack([and_old, and_new], dim=1)
5684
+ new_bit = heaviside((or_inp * or_w[:, bit]).sum(dim=1) + or_b[:, bit])
5685
+ mem_bits[:, bit] = new_bit
5686
+ return [bits_to_int([int(b) for b in mem_bits[i].tolist()]) for i in range(self.mem_bytes)]
5687
+
5688
+ # Conditions 0..7 map to (mux circuit, flag index into [Z, N, C, V]).
5689
+ # Odd conditions are the negated forms; the mux select is the complemented
5690
+ # flag (the circuits are plain per-bit muxes -- see control.* .inputs,
5691
+ # which name the select $not_zero / $not_carry / ... for the odd forms).
5692
+ _JCC = [("control.jz", 0), ("control.jnz", 0), ("control.jc", 2), ("control.jnc", 2),
5693
+ ("control.jn", 1), ("control.jp", 1), ("control.jv", 3), ("control.jnv", 3)]
5694
+
5695
+ def _jcc_pc(self, circuit: str, pc: int, target: int, sel: int) -> int:
5696
+ """Per-bit 2:1 mux over the PC: sel ? target : pc, addr_bits wide."""
5697
+ pc_bits = int_to_bits(pc, self.addr_bits)
5698
+ t_bits = int_to_bits(target, self.addr_bits)
5699
+ out = []
5700
+ for bit in range(self.addr_bits):
5701
+ bp = f"{circuit}.bit{bit}"
5702
+ not_sel = self.alu._g(f"{bp}.not_sel", [float(sel)])
5703
+ and_a = self.alu._g(f"{bp}.and_a", [float(pc_bits[bit]), not_sel])
5704
+ and_b = self.alu._g(f"{bp}.and_b", [float(t_bits[bit]), float(sel)])
5705
+ out.append(self.alu._g(f"{bp}.or", [and_a, and_b]))
5706
+ return bits_to_int(out)
5707
+
5708
+ def _sp_dec(self, sp: int) -> int:
5709
+ """SP - 1 through the control.push.sp_dec borrow chain."""
5710
+ bits = int_to_bits(sp, self.addr_bits)
5711
+ out = [0] * self.addr_bits
5712
+ borrow = 1
5713
+ for bit in range(self.addr_bits - 1, -1, -1):
5714
+ bp = f"control.push.sp_dec.bit{bit}"
5715
+ h_or = self.alu._g(f"{bp}.xor.layer1.or", [float(bits[bit]), float(borrow)])
5716
+ h_nand = self.alu._g(f"{bp}.xor.layer1.nand", [float(bits[bit]), float(borrow)])
5717
+ out[bit] = self.alu._g(f"{bp}.xor.layer2", [h_or, h_nand])
5718
+ borrow = self.alu._g(f"{bp}.borrow", [float(1 - bits[bit]), float(borrow)])
5719
+ return bits_to_int(out)
5720
+
5721
+ def _decode_op(self, opcode):
5722
+ """4-to-16 opcode one-hot through the gate decoder."""
5723
+ ob = [float((opcode >> j) & 1) for j in range(4)]
5724
+ return [self.alu._g(f"control.decode.op{n}", ob) for n in range(16)]
5725
+
5726
+ def _inc(self, pc, name, start):
5727
+ """PC + 2^start through the gate increment chain (LSB-first)."""
5728
+ ab = self.addr_bits
5729
+ pcb = [(pc >> k) & 1 for k in range(ab)]
5730
+ out = [0] * ab
5731
+ carry = 0
5732
+ for k in range(ab):
5733
+ if k < start:
5734
+ out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}", [float(pcb[k])])
5735
+ else:
5736
+ cin = 1 if k == start else carry
5737
+ o = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.or", [float(pcb[k]), float(cin)])
5738
+ nd = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.nand", [float(pcb[k]), float(cin)])
5739
+ out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer2", [o, nd])
5740
+ carry = self.alu._g(f"control.pcnext.{name}.bit{k}.carry", [float(pcb[k]), float(cin)])
5741
+ return sum(out[k] << k for k in range(ab))
5742
+
5743
+ def _pcnext(self, pc2, pc4, addr, jcc, use_pc4, use_addr, use_jcc):
5744
+ """Next-PC priority mux through the gates: use_addr ? addr : (use_jcc ? jcc : (use_pc4 ? pc4 : pc2))."""
5745
+ ab = self.addr_bits
5746
+ p2 = [(pc2 >> k) & 1 for k in range(ab)]
5747
+ p4 = [(pc4 >> k) & 1 for k in range(ab)]
5748
+ aB = [(addr >> k) & 1 for k in range(ab)]
5749
+ jB = [(jcc >> k) & 1 for k in range(ab)]
5750
+ out = [0] * ab
5751
+ for k in range(ab):
5752
+ ns = self.alu._g(f"control.pcnext.m_pc4.bit{k}.not_sel", [float(use_pc4)])
5753
+ aa = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_a", [float(p2[k]), ns])
5754
+ bb = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_b", [float(p4[k]), float(use_pc4)])
5755
+ m1 = self.alu._g(f"control.pcnext.m_pc4.bit{k}.or", [aa, bb])
5756
+ ns = self.alu._g(f"control.pcnext.m_jcc.bit{k}.not_sel", [float(use_jcc)])
5757
+ aa = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_a", [float(m1), ns])
5758
+ bb = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_b", [float(jB[k]), float(use_jcc)])
5759
+ m2 = self.alu._g(f"control.pcnext.m_jcc.bit{k}.or", [aa, bb])
5760
+ ns = self.alu._g(f"control.pcnext.m_addr.bit{k}.not_sel", [float(use_addr)])
5761
+ aa = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_a", [float(m2), ns])
5762
+ bb = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_b", [float(aB[k]), float(use_addr)])
5763
+ out[k] = self.alu._g(f"control.pcnext.m_addr.bit{k}.or", [aa, bb])
5764
+ return sum(out[k] << k for k in range(ab))
5765
+
5766
+ def step(self, state):
5767
+ if state["halted"]:
5768
+ return state
5769
+ s = dict(state)
5770
+ s["mem"] = state["mem"][:]
5771
+ s["regs"] = state["regs"][:]
5772
+ s["flags"] = state["flags"][:]
5773
+ addr_mask = (1 << self.addr_bits) - 1
5774
+ pc = s["pc"]
5775
+ hi = self.mem_read(s["mem"], pc & addr_mask)
5776
+ lo = self.mem_read(s["mem"], (pc + 1) & addr_mask)
5777
+ ir = ((hi & 0xFF) << 8) | (lo & 0xFF)
5778
+ opcode = (ir >> 12) & 0xF
5779
+ oh = self._decode_op(opcode) # gate opcode decode (one-hots)
5780
+ rd = (ir >> 10) & 0x3
5781
+ rs = (ir >> 8) & 0x3
5782
+ imm = ir & 0xFF
5783
+ addr = None
5784
+ if oh[0xA] or oh[0xB] or oh[0xC] or oh[0xD] or oh[0xE]:
5785
+ ah = self.mem_read(s["mem"], (pc + 2) & addr_mask)
5786
+ al = self.mem_read(s["mem"], (pc + 3) & addr_mask)
5787
+ addr = (((ah & 0xFF) << 8) | (al & 0xFF)) & addr_mask
5788
+ pc2 = self._inc(pc, "pc2", 1) # PC+2 through the gate chain
5789
+ pc4 = self._inc(pc, "pc4", 2) # PC+4 through the gate chain
5790
+ a = s["regs"][rd]
5791
+ b = s["regs"][rs]
5792
+ result = a
5793
+ carry = 0
5794
+ overflow = 0
5795
+ write_result = True
5796
+ jcc_val = 0
5797
+ if oh[0x0]:
5798
+ result, carry = self.alu.add8(a, b)
5799
+ overflow = 1 if (((a ^ result) & (b ^ result)) & 0x80) else 0
5800
+ elif oh[0x1]:
5801
+ result, carry = self.alu.sub8(a, b)
5802
+ overflow = 1 if (((a ^ b) & (a ^ result)) & 0x80) else 0
5803
+ elif oh[0x2]:
5804
+ result = self.alu.and8(a, b)
5805
+ elif oh[0x3]:
5806
+ result = self.alu.or8(a, b)
5807
+ elif oh[0x4]:
5808
+ result = self.alu.xor8(a, b)
5809
+ elif oh[0x5]:
5810
+ result = self.alu.shl8(a)
5811
+ elif oh[0x6]:
5812
+ result = self.alu.shr8(a)
5813
+ elif oh[0x7]:
5814
+ result = self.alu.mul8(a, b)
5815
+ elif oh[0x8]:
5816
+ result, _ = self.alu.div8(a, b)
5817
+ elif oh[0x9]: # CMP: set flags only
5818
+ r2, carry = self.alu.sub8(a, b)
5819
+ z = 1 if r2 == 0 else 0
5820
+ n = 1 if (r2 & 0x80) else 0
5821
+ v = 1 if (((a ^ b) & (a ^ r2)) & 0x80) else 0
5822
+ s["flags"] = [z, n, carry, v]
5823
+ write_result = False
5824
+ elif oh[0xA]: # LOAD
5825
+ result = self.mem_read(s["mem"], addr)
5826
+ elif oh[0xB]: # STORE
5827
+ s["mem"] = self.mem_write(s["mem"], addr, b & 0xFF)
5828
+ write_result = False
5829
+ elif oh[0xC]: # JMP (PC = addr via pcnext)
5830
+ write_result = False
5831
+ elif oh[0xD]: # JCC (PC via jcc mux + pcnext)
5832
+ cond = imm & 0x7
5833
+ circuit, flag_idx = self._JCC[cond]
5834
+ flag = s["flags"][flag_idx]
5835
+ sel = flag if cond % 2 == 0 else 1 - flag
5836
+ jcc_val = self._jcc_pc(circuit, pc4, addr, sel)
5837
+ write_result = False
5838
+ elif oh[0xE]: # CALL: push PC+4, PC = addr
5839
+ ret_addr = pc4 & 0xFFFF
5840
+ sp = s.get("sp", addr_mask)
5841
+ sp = self._sp_dec(sp)
5842
+ s["mem"] = self.mem_write(s["mem"], sp, (ret_addr >> 8) & 0xFF)
5843
+ sp = self._sp_dec(sp)
5844
+ s["mem"] = self.mem_write(s["mem"], sp, ret_addr & 0xFF)
5845
+ s["sp"] = sp
5846
+ write_result = False
5847
+ elif oh[0xF]: # HALT
5848
+ s["halted"] = True
5849
+ return s
5850
+
5851
+ if write_result:
5852
+ s["regs"][rd] = result & 0xFF
5853
+ if oh[0x0] or oh[0x1] or oh[0x7]:
5854
+ z = 1 if (result & 0xFF) == 0 else 0
5855
+ n = 1 if (result & 0x80) else 0
5856
+ s["flags"] = [z, n, carry, overflow]
5857
+ # PC sequencing through the gate mux.
5858
+ use_pc4 = 1 if (oh[0xA] or oh[0xB]) else 0
5859
+ use_addr = 1 if (oh[0xC] or oh[0xE]) else 0
5860
+ s["pc"] = self._pcnext(pc2, pc4, addr or 0, jcc_val, use_pc4, use_addr, oh[0xD])
5861
+ return s
5862
+
5863
+ def run(self, state, max_cycles=200):
5864
+ s = state
5865
+ cycles = 0
5866
+ while not s["halted"] and cycles < max_cycles:
5867
+ s = self.step(s)
5868
+ cycles += 1
5869
+ return s, cycles
5870
+
5871
+
5872
+ def _encode_instr(opcode, rd, rs, imm):
5873
+ return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm & 0xFF)
5874
+
5875
+
5876
+ def _w16(mem, addr, value):
5877
+ mem[addr] = (value >> 8) & 0xFF
5878
+ mem[addr + 1] = value & 0xFF
5879
+
5880
+
5881
+ PROGRAM_MIN_BYTES = 0x84 # code 0x00..0x1F + data 0x80..0x83
5882
+
5883
+
5884
+ def builtin_program(addr_bits: int) -> Tuple[List[int], int]:
5885
+ """Sum 5+4+3+2+1 via a loop."""
5886
+ if (1 << addr_bits) < PROGRAM_MIN_BYTES:
5887
+ raise ValueError(f"addr_bits={addr_bits} too small for builtin program")
5888
+ mem = [0] * (1 << addr_bits)
5889
+ mem[0x80] = 5 # initial counter
5890
+ mem[0x81] = 1 # decrement
5891
+ mem[0x82] = 0 # zero (for compare and accumulator init)
5892
+ # mem[0x83] is the output
5893
+ _w16(mem, 0x0000, _encode_instr(0xA, 1, 0, 0)); _w16(mem, 0x0002, 0x0080)
5894
+ _w16(mem, 0x0004, _encode_instr(0xA, 2, 0, 0)); _w16(mem, 0x0006, 0x0081)
5895
+ _w16(mem, 0x0008, _encode_instr(0xA, 3, 0, 0)); _w16(mem, 0x000A, 0x0082)
5896
+ _w16(mem, 0x000C, _encode_instr(0xA, 0, 0, 0)); _w16(mem, 0x000E, 0x0082)
5897
+ _w16(mem, 0x0010, _encode_instr(0x0, 0, 1, 0))
5898
+ _w16(mem, 0x0012, _encode_instr(0x1, 1, 2, 0))
5899
+ _w16(mem, 0x0014, _encode_instr(0x9, 1, 3, 0))
5900
+ _w16(mem, 0x0016, _encode_instr(0xD, 0, 0, 0x01)); _w16(mem, 0x0018, 0x0010)
5901
+ _w16(mem, 0x001A, _encode_instr(0xB, 0, 0, 0)); _w16(mem, 0x001C, 0x0083)
5902
+ _w16(mem, 0x001E, _encode_instr(0xF, 0, 0, 0))
5903
+ return mem, 15
5904
+
5905
+
5906
+ # ---------------------------------------------------------------------------
5907
+ # Eval driver
5908
+ # ---------------------------------------------------------------------------
5909
+
5910
+ def _file_fingerprint(path: Path) -> str:
5911
+ """Stable cache key for a safetensors file: sha256 of its content."""
5912
+ import hashlib
5913
+ h = hashlib.sha256()
5914
+ with open(path, "rb") as f:
5915
+ for chunk in iter(lambda: f.read(1 << 20), b""):
5916
+ h.update(chunk)
5917
+ return h.hexdigest()
5918
+
5919
+
5920
+ def _cache_key(path: Path, opts: Dict[str, Any]) -> str:
5921
+ """Cache key combining file content with the relevant evaluation options."""
5922
+ fp = _file_fingerprint(path)
5923
+ opt_str = json.dumps(opts, sort_keys=True)
5924
+ import hashlib
5925
+ suffix = hashlib.sha256(opt_str.encode("utf-8")).hexdigest()[:8]
5926
+ return f"{fp}_{suffix}"
5927
+
5928
+
5929
+ def _load_cache(cache_dir: Path, key: str) -> Dict[str, Any] | None:
5930
+ p = cache_dir / f"{key}.json"
5931
+ if not p.exists():
5932
+ return None
5933
+ try:
5934
+ return json.loads(p.read_text(encoding="utf-8"))
5935
+ except (json.JSONDecodeError, OSError):
5936
+ return None
5937
+
5938
+
5939
+ def _save_cache(cache_dir: Path, key: str, payload: Dict[str, Any]) -> None:
5940
+ cache_dir.mkdir(parents=True, exist_ok=True)
5941
+ p = cache_dir / f"{key}.json"
5942
+ try:
5943
+ p.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
5944
+ except OSError:
5945
+ pass
5946
+
5947
+
5948
+ def list_safetensors(path: Path) -> List[Path]:
5949
+ if path.is_file():
5950
+ return [path]
5951
+ if path.is_dir():
5952
+ return sorted(p for p in path.glob("*.safetensors") if p.is_file())
5953
+ return []
5954
+
5955
+
5956
+ # Standalone machines carry their own circuit inventory and are verified by
5957
+ # their own scripts, not the gate-fitness suite. Each maps to the tool that
5958
+ # checks it.
5959
+ MACHINE_VERIFIER = {
5960
+ "subleq8": "src/machines.py",
5961
+ "rv32": "src/machines.py",
5962
+ "matrix8": "src/machines.py matrix8",
5963
+ "subleq8io": "src/host.py constructor8",
5964
+ "reflect": "src/reflect.py",
5965
+ "attractor": "src/automata.py attractor",
5966
+ "reversible": "src/automata.py reversible",
5967
+ "ca": "src/automata.py ca",
5968
+ "tile": "src/automata.py tile",
5969
+ }
5970
+
5971
+
5972
+ def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_program: bool) -> Dict:
5973
+ out: Dict = {"path": str(path), "filename": path.name}
5974
+ # Skip standalone machines cleanly rather than error on missing standard gates.
5975
+ with safe_open(str(path), framework="pt") as f:
5976
+ meta = f.metadata() or {}
5977
+ if meta.get("machine"):
5978
+ m = meta["machine"]
5979
+ out.update(status="SKIP", machine=m,
5980
+ note=f"standalone machine — verify with {MACHINE_VERIFIER.get(m, 'machines.py')}")
5981
+ return out
5982
+ try:
5983
+ tensors = load_model(str(path))
5984
+ except Exception as e:
5985
+ out.update(error=f"load failed: {e}", status="ERROR")
5986
+ return out
5987
+
5988
+ manifest = get_manifest(tensors)
5989
+ out.update(
5990
+ size_mb=path.stat().st_size / (1024 * 1024),
5991
+ tensors=len(tensors),
5992
+ params=sum(t.numel() for t in tensors.values()),
5993
+ manifest=manifest,
5994
+ )
5995
+
5996
+ # Move to device
5997
+ tensors = {k: v.to(device) for k, v in tensors.items()}
5998
+
5999
+ try:
6000
+ evaluator = BatchedFitnessEvaluator(device=device, model_path=str(path), tensors=tensors)
6001
+ population = create_population(tensors, pop_size=pop_size, device=device)
6002
+ t0 = time.perf_counter()
6003
+ fitness = evaluator.evaluate(population, debug=debug)
6004
+ elapsed = time.perf_counter() - t0
6005
+ f0 = float(fitness[0].item()) if pop_size == 1 else float(fitness.mean().item())
6006
+ out.update(
6007
+ fitness=f0,
6008
+ total_tests=evaluator.total_tests,
6009
+ elapsed_s=elapsed,
6010
+ categories={k: (float(v[0]), int(v[1])) for k, v in evaluator.category_scores.items()},
6011
+ status="PASS" if f0 >= 0.9999 else "FAIL",
6012
+ )
6013
+ except Exception as e:
6014
+ out.update(error=f"eval failed: {type(e).__name__}: {e}", status="ERROR")
6015
+ return out
6016
+
6017
+ # Optional: CPU program test (8-bit CPU primitives are in every variant)
6018
+ if run_cpu_program:
6019
+ if manifest["memory_bytes"] >= PROGRAM_MIN_BYTES:
6020
+ try:
6021
+ cpu_tensors = {k: v.cpu() for k, v in tensors.items()}
6022
+ cpu = GenericThresholdCPU(cpu_tensors)
6023
+ mem, expected = builtin_program(manifest["addr_bits"])
6024
+ state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": mem, "halted": False}
6025
+ t0 = time.perf_counter()
6026
+ final, cycles = cpu.run(state, max_cycles=200)
6027
+ cpu_elapsed = time.perf_counter() - t0
6028
+ got = final["mem"][0x83]
6029
+ out["cpu_program"] = {
6030
+ "ok": got == expected,
6031
+ "got": got,
6032
+ "expected": expected,
6033
+ "cycles": cycles,
6034
+ "elapsed_s": cpu_elapsed,
6035
+ }
6036
+ if got != expected:
6037
+ out["status"] = "FAIL"
6038
+ except Exception as e:
6039
+ out["cpu_program"] = {"error": str(e)}
6040
+ else:
6041
+ out["cpu_program"] = {"skipped": f"mem={manifest['memory_bytes']}B < {PROGRAM_MIN_BYTES}"}
6042
+
6043
+ # Wider-ALU chain test for 16/32-bit variants
6044
+ bits = manifest["data_bits"]
6045
+ if bits in (16, 32):
6046
+ try:
6047
+ alu_tensors = {k: v.cpu() for k, v in tensors.items()}
6048
+ alu = GenericThresholdALU(alu_tensors, bits)
6049
+ t0 = time.perf_counter()
6050
+ if bits == 16:
6051
+ x, y = 1234, 5678
6052
+ z, _ = alu.add_n(x, y, 16); assert z == (x + y) & 0xFFFF
6053
+ w, _ = alu.sub_n(z, x, 16); assert w == (z - x) & 0xFFFF, (w, z - x)
6054
+ gt = alu.cmp_n(z, x, "greaterthan", 16); assert gt == 1
6055
+ lt = alu.cmp_n(x, z, "lessthan", 16); assert lt == 1
6056
+ eq = alu.cmp_n(w, y, "eq", 16); assert eq == 1
6057
+ p = alu.mul_n(123, 5, 16); assert p == (123 * 5) & 0xFFFF
6058
+ else: # 32
6059
+ x, y = 1_000_000, 999_000
6060
+ z, _ = alu.sub_n(x, y, 32); assert z == 1_000
6061
+ s, _ = alu.add_n(z, x, 32); assert s == 1_001_000
6062
+ p = alu.mul_n(z, 100, 32); assert p == 100_000
6063
+ gt = alu.cmp_n(x, y, "greaterthan", 32); assert gt == 1
6064
+ lt = alu.cmp_n(y, x, "lessthan", 32); assert lt == 1
6065
+ eq = alu.cmp_n(p, 100_000, "equality", 32); assert eq == 1
6066
+ chain_dt = time.perf_counter() - t0
6067
+ out[f"alu_chain_{bits}"] = {"ok": True, "elapsed_s": chain_dt}
6068
+ except AssertionError as e:
6069
+ out[f"alu_chain_{bits}"] = {"ok": False, "error": f"chain mismatch: {e}"}
6070
+ out["status"] = "FAIL"
6071
+ except Exception as e:
6072
+ out[f"alu_chain_{bits}"] = {"ok": False, "error": f"{type(e).__name__}: {e}"}
6073
+ out["status"] = "FAIL"
6074
+
6075
+ return out
6076
+
6077
+
6078
+ def print_row(r: Dict, show_cpu: bool) -> None:
6079
+ if r.get("status") == "SKIP":
6080
+ m = r.get("machine", "machine")
6081
+ print(f" {r['filename']:<48} SKIP ({m} — verify with {MACHINE_VERIFIER.get(m, 'machines.py')})")
6082
+ return
6083
+ if "error" in r:
6084
+ print(f" {r['filename']:<48} ERROR: {r['error'][:80]}")
6085
+ return
6086
+ m = r["manifest"]
6087
+ fit = f"{r['fitness']:.4f}" if r.get("fitness") is not None else "n/a"
6088
+ cpu_col = ""
6089
+ if show_cpu and "cpu_program" in r:
6090
+ cp = r["cpu_program"]
6091
+ if cp.get("ok"):
6092
+ cpu_col = f" CPU OK ({cp['cycles']}cyc/{cp['elapsed_s']:.1f}s)"
6093
+ elif "skipped" in cp:
6094
+ cpu_col = f" CPU SKIP"
6095
+ elif "error" in cp:
6096
+ cpu_col = f" CPU ERR"
6097
+ else:
6098
+ cpu_col = f" CPU FAIL ({cp.get('got')}!={cp.get('expected')})"
6099
+ chain_col = ""
6100
+ if show_cpu:
6101
+ for bits in (16, 32):
6102
+ key = f"alu_chain_{bits}"
6103
+ if key in r:
6104
+ ch = r[key]
6105
+ if ch.get("ok"):
6106
+ chain_col = f" ALU{bits} OK ({ch['elapsed_s']:.2f}s)"
6107
+ else:
6108
+ chain_col = f" ALU{bits} FAIL"
6109
+ print(
6110
+ f" {r['filename']:<48} d={m['data_bits']:>2}b a={m['addr_bits']:>2}b "
6111
+ f"mem={m['memory_bytes']:>6}B size={r['size_mb']:>6.1f}MB "
6112
+ f"params={r['params']:>10,} fit={fit:>6} tests={r['total_tests']:>5} "
6113
+ f"{r['status']:>5}{cpu_col}{chain_col}"
6114
+ )
6115
+
6116
+
6117
+ def main_all() -> int:
6118
+ parser = argparse.ArgumentParser(description="Variant-agnostic eval harness")
6119
+ parser.add_argument("path", help="Path to .safetensors file or directory of files")
6120
+ parser.add_argument("--device", default="cpu", help="cpu (default) or cuda")
6121
+ parser.add_argument("--pop_size", type=int, default=1)
6122
+ parser.add_argument("--debug", action="store_true", help="Per-circuit detail per file")
6123
+ parser.add_argument("--cpu-program", action="store_true",
6124
+ help="Also run a small assembled program through the threshold CPU "
6125
+ "(any variant with >= 132 B memory), plus a chained 16- or "
6126
+ "32-bit ALU sequence on wider variants")
6127
+ parser.add_argument("--json", action="store_true", help="Emit JSON results to stdout instead of a table")
6128
+ parser.add_argument("--cache-dir", default=".eval_cache",
6129
+ help="Directory for hash-keyed result cache "
6130
+ "(default: ./.eval_cache). Set to '' to disable.")
6131
+ parser.add_argument("--no-cache", action="store_true",
6132
+ help="Disable the result cache for this run.")
6133
+ args = parser.parse_args()
6134
+
6135
+ files = list_safetensors(Path(args.path))
6136
+ if not files:
6137
+ print(f"No .safetensors files found under {args.path}", file=sys.stderr)
6138
+ return 2
6139
+
6140
+ print(f"Evaluating {len(files)} file(s) on {args.device}\n")
6141
+ cache_enabled = bool(args.cache_dir) and not args.no_cache
6142
+ cache_dir = Path(args.cache_dir) if cache_enabled else None
6143
+ cache_opts = {
6144
+ "device": args.device,
6145
+ "pop_size": args.pop_size,
6146
+ "cpu_program": bool(args.cpu_program),
6147
+ }
6148
+ cache_hits = 0
6149
+ results = []
6150
+ fail_count = 0
6151
+ for f in files:
6152
+ print(f"=== {f.name}")
6153
+ cached = None
6154
+ key = None
6155
+ if cache_enabled:
6156
+ try:
6157
+ key = _cache_key(f, cache_opts)
6158
+ cached = _load_cache(cache_dir, key)
6159
+ except OSError:
6160
+ cached = None
6161
+ if cached is not None:
6162
+ r = cached
6163
+ cache_hits += 1
6164
+ print(f" (cache hit)")
6165
+ else:
6166
+ r = evaluate_one(f, device=args.device, pop_size=args.pop_size,
6167
+ debug=args.debug, run_cpu_program=args.cpu_program)
6168
+ # Don't cache ERROR results: a transient failure (OOM, interrupt)
6169
+ # would otherwise be replayed on every rerun of the same file.
6170
+ if cache_enabled and key is not None and r.get("status") != "ERROR":
6171
+ _save_cache(cache_dir, key, r)
6172
+ results.append(r)
6173
+ print_row(r, show_cpu=args.cpu_program)
6174
+ if r.get("status") not in ("PASS", "SKIP"):
6175
+ fail_count += 1
6176
+
6177
+ if args.json:
6178
+ # Make it serialisable
6179
+ for r in results:
6180
+ r["manifest"] = {k: (int(v) if isinstance(v, float) and v.is_integer() else v)
6181
+ for k, v in r.get("manifest", {}).items()}
6182
+ print(json.dumps(results, indent=2, default=str))
6183
+ return fail_count
6184
+
6185
+ # Summary
6186
+ print()
6187
+ print("=" * 100)
6188
+ print(" SUMMARY")
6189
+ print("=" * 100)
6190
+ for r in results:
6191
+ print_row(r, show_cpu=args.cpu_program)
6192
+
6193
+ print()
6194
+ if fail_count == 0:
6195
+ print(f"ALL {len(files)} variants PASS")
6196
+ else:
6197
+ print(f"{fail_count}/{len(files)} variants FAIL")
6198
+ if cache_enabled:
6199
+ print(f"(cache: {cache_hits}/{len(files)} hits, dir={cache_dir})")
6200
+ return fail_count
6201
+
6202
+
6203
+ # ----------------------------------------------------------------------------
6204
+ # Mini assembler
6205
+ # ----------------------------------------------------------------------------
6206
+
6207
+ _OPCODE_NAMES = {
6208
+ "add": 0x0, "sub": 0x1, "and": 0x2, "or": 0x3, "xor": 0x4,
6209
+ "shl": 0x5, "shr": 0x6, "mul": 0x7, "div": 0x8, "cmp": 0x9,
6210
+ "load": 0xA, "store": 0xB, "jmp": 0xC, "jcc": 0xD,
6211
+ "call": 0xE, "halt": 0xF,
6212
+ }
6213
+
6214
+ _COND = {"jz": 0, "jnz": 1, "jc": 2, "jnc": 3,
6215
+ "jn": 4, "jp": 5, "jv": 6, "jnv": 7}
6216
+
6217
+
6218
+ def _enc(opcode: int, rd: int = 0, rs: int = 0, imm: int = 0) -> int:
6219
+ return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm & 0xFF)
6220
+
6221
+
6222
+ class Asm:
6223
+ """Tiny assembler for the threshold-computer ISA."""
6224
+
6225
+ def __init__(self, size: int):
6226
+ self.mem: List[int] = [0] * size
6227
+ self.pc: int = 0
6228
+ self.labels: Dict[str, int] = {}
6229
+ self.fixups: List[Tuple[int, str]] = []
6230
+
6231
+ def org(self, addr: int) -> None:
6232
+ self.pc = addr
6233
+
6234
+ def label(self, name: str) -> None:
6235
+ if name in self.labels:
6236
+ raise ValueError(f"duplicate label: {name}")
6237
+ self.labels[name] = self.pc
6238
+
6239
+ def db(self, *values: int) -> None:
6240
+ for v in values:
6241
+ self.mem[self.pc] = v & 0xFF
6242
+ self.pc += 1
6243
+
6244
+ def dw(self, value: int) -> None:
6245
+ self.mem[self.pc] = (value >> 8) & 0xFF
6246
+ self.mem[self.pc + 1] = value & 0xFF
6247
+ self.pc += 2
6248
+
6249
+ def daddr(self, label: str) -> None:
6250
+ self.fixups.append((self.pc, label))
6251
+ self.dw(0)
6252
+
6253
+ # --- ALU ops (no immediate) ---
6254
+ def _alu(self, op: int, rd: int, rs: int) -> None:
6255
+ self.dw(_enc(op, rd, rs))
6256
+
6257
+ def add(self, rd: int, rs: int) -> None: self._alu(0x0, rd, rs)
6258
+ def sub(self, rd: int, rs: int) -> None: self._alu(0x1, rd, rs)
6259
+ def and_(self, rd: int, rs: int) -> None: self._alu(0x2, rd, rs)
6260
+ def or_(self, rd: int, rs: int) -> None: self._alu(0x3, rd, rs)
6261
+ def xor(self, rd: int, rs: int) -> None: self._alu(0x4, rd, rs)
6262
+ def shl(self, rd: int) -> None: self._alu(0x5, rd, 0)
6263
+ def shr(self, rd: int) -> None: self._alu(0x6, rd, 0)
6264
+ def mul(self, rd: int, rs: int) -> None: self._alu(0x7, rd, rs)
6265
+ def cmp(self, rd: int, rs: int) -> None: self._alu(0x9, rd, rs)
6266
+
6267
+ # --- Memory + control (address-extended) ---
6268
+ def load(self, rd: int, label: str) -> None:
6269
+ self.dw(_enc(0xA, rd, 0)); self.daddr(label)
6270
+
6271
+ def store(self, rs: int, label: str) -> None:
6272
+ self.dw(_enc(0xB, 0, rs)); self.daddr(label)
6273
+
6274
+ def jmp(self, label: str) -> None:
6275
+ self.dw(_enc(0xC)); self.daddr(label)
6276
+
6277
+ def jcc(self, cond: str, label: str) -> None:
6278
+ self.dw(_enc(0xD, 0, 0, _COND[cond])); self.daddr(label)
6279
+
6280
+ def jz(self, label: str) -> None: self.jcc("jz", label)
6281
+ def jnz(self, label: str) -> None: self.jcc("jnz", label)
6282
+ def jc(self, label: str) -> None: self.jcc("jc", label)
6283
+ def jnc(self, label: str) -> None: self.jcc("jnc", label)
6284
+ def jn(self, label: str) -> None: self.jcc("jn", label)
6285
+ def jp(self, label: str) -> None: self.jcc("jp", label)
6286
+ def jv(self, label: str) -> None: self.jcc("jv", label)
6287
+ def jnv(self, label: str) -> None: self.jcc("jnv", label)
6288
+
6289
+ def call(self, label: str) -> None:
6290
+ self.dw(_enc(0xE)); self.daddr(label)
6291
+
6292
+ def halt(self) -> None:
6293
+ self.dw(_enc(0xF))
6294
+
6295
+ def assemble(self) -> List[int]:
6296
+ for offset, label in self.fixups:
6297
+ if label not in self.labels:
6298
+ raise ValueError(f"undefined label: {label}")
6299
+ target = self.labels[label]
6300
+ self.mem[offset] = (target >> 8) & 0xFF
6301
+ self.mem[offset + 1] = target & 0xFF
6302
+ return self.mem
6303
+
6304
+
6305
+ # ----------------------------------------------------------------------------
6306
+ # Programs
6307
+ # ----------------------------------------------------------------------------
6308
+
6309
+ ProgramResult = Tuple[List[int], Dict[int, int], int, str]
6310
+
6311
+
6312
+ def fib(n: int = 11, mem_size: int = 256) -> ProgramResult:
6313
+ """Iterative Fibonacci F(N), 8-bit wrap."""
6314
+ a = Asm(mem_size)
6315
+
6316
+ a.org(0)
6317
+ a.load(2, "n_addr") # R2 = n
6318
+ a.load(0, "zero_addr") # R0 = 0 = F(0)
6319
+ a.load(1, "one_addr") # R1 = 1 = F(1)
6320
+ a.label("loop")
6321
+ a.load(3, "zero_addr") # R3 = 0
6322
+ a.cmp(2, 3) # n == 0?
6323
+ a.jz("done")
6324
+ a.load(3, "zero_addr") # R3 = 0
6325
+ a.add(3, 1) # R3 = b (saved old b)
6326
+ a.add(1, 0) # R1 = a + b (new b)
6327
+ a.load(0, "zero_addr") # R0 = 0
6328
+ a.add(0, 3) # R0 = old b (new a)
6329
+ a.load(3, "one_addr") # R3 = 1
6330
+ a.sub(2, 3) # n--
6331
+ a.jmp("loop")
6332
+ a.label("done")
6333
+ a.store(0, "out") # OUT = a
6334
+ a.halt()
6335
+
6336
+ a.org(0x80)
6337
+ a.label("zero_addr"); a.db(0)
6338
+ a.label("one_addr"); a.db(1)
6339
+ a.label("n_addr"); a.db(n)
6340
+ a.label("out"); a.db(0)
6341
+
6342
+ mem = a.assemble()
6343
+ expected_a = 0
6344
+ aa, bb = 0, 1
6345
+ for _ in range(n):
6346
+ aa, bb = bb, (aa + bb) & 0xFF
6347
+ expected_a = aa
6348
+ return mem, {a.labels["out"]: expected_a}, 16 * (n + 2), f"Fibonacci F({n}) = {expected_a}"
6349
+
6350
+
6351
+ def sum_n(n: int = 10, mem_size: int = 256) -> ProgramResult:
6352
+ """Compute 1 + 2 + ... + N using the Z flag from SUB to terminate."""
6353
+ a = Asm(mem_size)
6354
+
6355
+ a.org(0)
6356
+ a.load(0, "zero_addr") # acc = 0
6357
+ a.load(1, "n_addr") # i = n
6358
+ a.load(2, "one_addr") # 1
6359
+ a.label("loop")
6360
+ a.add(0, 1) # acc += i
6361
+ a.sub(1, 2) # i-- (Z set when i becomes 0)
6362
+ a.jnz("loop")
6363
+ a.store(0, "out")
6364
+ a.halt()
6365
+
6366
+ a.org(0x80)
6367
+ a.label("zero_addr"); a.db(0)
6368
+ a.label("one_addr"); a.db(1)
6369
+ a.label("n_addr"); a.db(n)
6370
+ a.label("out"); a.db(0)
6371
+
6372
+ mem = a.assemble()
6373
+ expected = (n * (n + 1) // 2) & 0xFF
6374
+ return mem, {a.labels["out"]: expected}, 4 + 4 * n, f"sum 1..{n} = {expected}"
6375
+
6376
+
6377
+ def self_mod_jmp(mem_size: int = 256) -> ProgramResult:
6378
+ """Self-modifying code: writes the JMP target's low byte at runtime."""
6379
+ a = Asm(mem_size)
6380
+
6381
+ a.org(0)
6382
+ a.label("start")
6383
+
6384
+ # Forward-declare the_jmp's LSB address. The first two instructions are
6385
+ # each 4 bytes; the_jmp follows them, so the_jmp = pc + 8 from start.
6386
+ # The JMP's address word is at the_jmp + 2; the LSB byte is at the_jmp + 3.
6387
+ a.labels["jmp_target_lsb"] = a.pc + 8 + 3
6388
+
6389
+ a.load(0, "new_lsb") # R0 = LSB of path_b address (4 bytes)
6390
+ a.store(0, "jmp_target_lsb") # patch the JMP's LSB (4 bytes)
6391
+ a.label("the_jmp")
6392
+ a.jmp("path_a") # initially -> path_a; after patch -> path_b
6393
+ a.label("path_a")
6394
+ a.load(1, "val_a"); a.store(1, "out"); a.halt()
6395
+ a.label("path_b")
6396
+ a.load(1, "val_b"); a.store(1, "out"); a.halt()
6397
+
6398
+ a.org(0x80)
6399
+ a.label("val_a"); a.db(0xAA)
6400
+ a.label("val_b"); a.db(0xBB)
6401
+ # new_lsb_word stores path_b's full 16-bit address; new_lsb labels the LSB byte.
6402
+ a.label("new_lsb_word"); a.daddr("path_b")
6403
+ a.labels["new_lsb"] = a.labels["new_lsb_word"] + 1
6404
+ a.label("out"); a.db(0)
6405
+
6406
+ mem = a.assemble()
6407
+ return mem, {a.labels["out"]: 0xBB}, 30, "self-modifying JMP target"
6408
+
6409
+
6410
+ def all_branches(mem_size: int = 256) -> ProgramResult:
6411
+ """Drive all eight conditional jumps; each path writes a unique marker."""
6412
+ a = Asm(mem_size)
6413
+
6414
+ a.org(0)
6415
+ # ----- JZ: equal compare -> Z=1 -----
6416
+ a.load(0, "v5"); a.load(1, "v5"); a.cmp(0, 1); a.jz("ok_jz"); a.jmp("fail")
6417
+ a.label("ok_jz"); a.load(2, "m_a0"); a.store(2, "out0")
6418
+
6419
+ # ----- JNZ: unequal compare -> Z=0 -----
6420
+ a.load(0, "v5"); a.load(1, "v3"); a.cmp(0, 1); a.jnz("ok_jnz"); a.jmp("fail")
6421
+ a.label("ok_jnz"); a.load(2, "m_a1"); a.store(2, "out1")
6422
+
6423
+ # ----- JC: 255+1 = 0 with carry -----
6424
+ a.load(0, "v255"); a.load(1, "v1"); a.add(0, 1); a.jc("ok_jc"); a.jmp("fail")
6425
+ a.label("ok_jc"); a.load(2, "m_a2"); a.store(2, "out2")
6426
+
6427
+ # ----- JNC: 1+1 = 2, no carry -----
6428
+ a.load(0, "v1"); a.load(1, "v1"); a.add(0, 1); a.jnc("ok_jnc"); a.jmp("fail")
6429
+ a.label("ok_jnc"); a.load(2, "m_a3"); a.store(2, "out3")
6430
+
6431
+ # ----- JN: 0 - 1 = 0xFF, MSB set -----
6432
+ a.load(0, "v0"); a.load(1, "v1"); a.sub(0, 1); a.jn("ok_jn"); a.jmp("fail")
6433
+ a.label("ok_jn"); a.load(2, "m_a4"); a.store(2, "out4")
6434
+
6435
+ # ----- JP: 0 + 1 = 1, MSB clear -----
6436
+ a.load(0, "v0"); a.load(1, "v1"); a.add(0, 1); a.jp("ok_jp"); a.jmp("fail")
6437
+ a.label("ok_jp"); a.load(2, "m_a5"); a.store(2, "out5")
6438
+
6439
+ # ----- JV: 127 + 1 = 128, signed overflow -----
6440
+ a.load(0, "v127"); a.load(1, "v1"); a.add(0, 1); a.jv("ok_jv"); a.jmp("fail")
6441
+ a.label("ok_jv"); a.load(2, "m_a6"); a.store(2, "out6")
6442
+
6443
+ # ----- JNV: 1 + 1 = 2, no signed overflow -----
6444
+ a.load(0, "v1"); a.load(1, "v1"); a.add(0, 1); a.jnv("ok_jnv"); a.jmp("fail")
6445
+ a.label("ok_jnv"); a.load(2, "m_a7"); a.store(2, "out7")
6446
+
6447
+ a.jmp("end")
6448
+ a.label("fail")
6449
+ a.load(2, "v_fail"); a.store(2, "fail_addr"); a.halt()
6450
+ a.label("end")
6451
+ a.halt()
6452
+
6453
+ # Code runs to ~0xDF; data starts safely after that.
6454
+ a.org(0xE0)
6455
+ a.label("v0"); a.db(0)
6456
+ a.label("v1"); a.db(1)
6457
+ a.label("v3"); a.db(3)
6458
+ a.label("v5"); a.db(5)
6459
+ a.label("v127"); a.db(127)
6460
+ a.label("v255"); a.db(255)
6461
+ a.label("m_a0"); a.db(0xA0)
6462
+ a.label("m_a1"); a.db(0xA1)
6463
+ a.label("m_a2"); a.db(0xA2)
6464
+ a.label("m_a3"); a.db(0xA3)
6465
+ a.label("m_a4"); a.db(0xA4)
6466
+ a.label("m_a5"); a.db(0xA5)
6467
+ a.label("m_a6"); a.db(0xA6)
6468
+ a.label("m_a7"); a.db(0xA7)
6469
+ a.label("v_fail"); a.db(0xEE)
6470
+ a.label("out0"); a.db(0)
6471
+ a.label("out1"); a.db(0)
6472
+ a.label("out2"); a.db(0)
6473
+ a.label("out3"); a.db(0)
6474
+ a.label("out4"); a.db(0)
6475
+ a.label("out5"); a.db(0)
6476
+ a.label("out6"); a.db(0)
6477
+ a.label("out7"); a.db(0)
6478
+ a.label("fail_addr"); a.db(0)
6479
+
6480
+ mem = a.assemble()
6481
+ expected = {
6482
+ a.labels["out0"]: 0xA0,
6483
+ a.labels["out1"]: 0xA1,
6484
+ a.labels["out2"]: 0xA2,
6485
+ a.labels["out3"]: 0xA3,
6486
+ a.labels["out4"]: 0xA4,
6487
+ a.labels["out5"]: 0xA5,
6488
+ a.labels["out6"]: 0xA6,
6489
+ a.labels["out7"]: 0xA7,
6490
+ a.labels["fail_addr"]: 0, # must remain zero
6491
+ }
6492
+ return mem, expected, 200, "all 8 conditional jumps (JZ/JNZ/JC/JNC/JN/JP/JV/JNV)"
6493
+
6494
+
6495
+ def call_pushes_pc(mem_size: int = 256) -> ProgramResult:
6496
+ """Verify CALL pushes the return address (next-instruction PC) onto the stack."""
6497
+ a = Asm(mem_size)
6498
+
6499
+ a.org(0)
6500
+ a.label("caller")
6501
+ a.load(0, "marker_val")
6502
+ a.store(0, "marker_addr") # write before CALL
6503
+ a.label("call_site")
6504
+ a.call("callee")
6505
+ # If CALL did not transfer control, this fallthrough store would write 0xDD:
6506
+ a.load(0, "noret_val")
6507
+ a.store(0, "noret_addr")
6508
+ a.halt()
6509
+
6510
+ a.label("callee")
6511
+ a.load(0, "callee_val")
6512
+ a.store(0, "callee_addr")
6513
+ a.halt()
6514
+
6515
+ a.org(0x40)
6516
+ a.label("marker_val"); a.db(0x11)
6517
+ a.label("marker_addr"); a.db(0)
6518
+ a.label("noret_val"); a.db(0xDD)
6519
+ a.label("noret_addr"); a.db(0)
6520
+ a.label("callee_val"); a.db(0x22)
6521
+ a.label("callee_addr"); a.db(0)
6522
+
6523
+ mem = a.assemble()
6524
+ addr_mask = mem_size - 1
6525
+ return_addr = a.labels["call_site"] + 4 # 4-byte CALL instruction
6526
+ expected = {
6527
+ a.labels["marker_addr"]: 0x11, # pre-CALL store ran
6528
+ a.labels["callee_addr"]: 0x22, # callee reached
6529
+ a.labels["noret_addr"]: 0, # fallthrough did not run
6530
+ (addr_mask - 2) & addr_mask: return_addr & 0xFF, # ret LSB on stack
6531
+ (addr_mask - 1) & addr_mask: (return_addr >> 8) & 0xFF, # ret MSB
6532
+ }
6533
+ return mem, expected, 30, "CALL pushes return PC onto stack"
6534
+
6535
+
6536
+ def bubble_sort_4(mem_size: int = 256) -> ProgramResult:
6537
+ """Sort a 4-byte array using unrolled compare-swap (3 passes of 3 compares)."""
6538
+ a = Asm(mem_size)
6539
+
6540
+ addrs = ["a0", "a1", "a2", "a3"]
6541
+
6542
+ a.org(0)
6543
+ for _outer in range(3):
6544
+ for i in range(3):
6545
+ x, y = addrs[i], addrs[i + 1]
6546
+ # Load pair
6547
+ a.load(0, x) # R0 = A[i]
6548
+ a.load(1, y) # R1 = A[i+1]
6549
+ a.cmp(0, 1) # compare A[i] - A[i+1]
6550
+ # If A[i] <= A[i+1] (Z=1 or C=0 from sub-style cmp), skip swap
6551
+ # SUB sets carry when no borrow (a >= b). So:
6552
+ # a > b iff Z=0 and a >= b -> Z=0 and C=1 (the sub didn't borrow)
6553
+ # We want to swap when a > b. JNC (no carry / borrow) means a < b -> skip swap.
6554
+ # If a == b (Z=1) we also skip. So: jump-skip when JZ OR JNC.
6555
+ # Easier: compute (a > b) by checking C=1 AND Z=0. Use JZ to skip on equal,
6556
+ # then JNC to skip on a < b. Otherwise fall through to swap.
6557
+ skip_lbl = f"skip_{_outer}_{i}"
6558
+ a.jz(skip_lbl) # equal -> skip
6559
+ a.jnc(skip_lbl) # a < b (sub borrowed) -> skip
6560
+ # swap: store R0 -> y, R1 -> x
6561
+ a.store(1, x)
6562
+ a.store(0, y)
6563
+ a.label(skip_lbl)
6564
+ a.halt()
6565
+
6566
+ # Initial unsorted array; code runs to ~0xEC, so data starts at 0xF0.
6567
+ initial = [42, 7, 200, 19]
6568
+ a.org(0xF0)
6569
+ for name, val in zip(addrs, initial):
6570
+ a.label(name); a.db(val)
6571
+
6572
+ mem = a.assemble()
6573
+ sorted_vals = sorted(initial)
6574
+ expected = {a.labels[name]: v for name, v in zip(addrs, sorted_vals)}
6575
+ return mem, expected, 800, f"bubble sort {initial} -> {sorted_vals}"
6576
+
6577
+
6578
+ def cross_check_mul(mem_size: int = 256) -> ProgramResult:
6579
+ """Cross-check the threshold MUL circuit against repeated ADD."""
6580
+ a = Asm(mem_size)
6581
+ A_VAL = 17
6582
+ B_VAL = 9
6583
+ expected_product = (A_VAL * B_VAL) & 0xFF
6584
+
6585
+ a.org(0)
6586
+ # --- direct multiply ---
6587
+ a.load(0, "A")
6588
+ a.load(1, "B")
6589
+ a.mul(0, 1)
6590
+ a.store(0, "out_mul")
6591
+ # --- repeated-add multiply ---
6592
+ a.load(0, "zero") # acc = 0
6593
+ a.load(1, "A") # addend
6594
+ a.load(2, "B") # counter
6595
+ a.load(3, "one") # 1
6596
+ a.label("rep_loop")
6597
+ a.add(0, 1) # acc += A
6598
+ a.sub(2, 3) # B--
6599
+ a.jnz("rep_loop")
6600
+ a.store(0, "out_add")
6601
+ a.halt()
6602
+
6603
+ a.org(0x80)
6604
+ a.label("A"); a.db(A_VAL)
6605
+ a.label("B"); a.db(B_VAL)
6606
+ a.label("zero"); a.db(0)
6607
+ a.label("one"); a.db(1)
6608
+ a.label("out_mul"); a.db(0)
6609
+ a.label("out_add"); a.db(0)
6610
+
6611
+ mem = a.assemble()
6612
+ expected = {
6613
+ a.labels["out_mul"]: expected_product,
6614
+ a.labels["out_add"]: expected_product,
6615
+ }
6616
+ return mem, expected, 80, f"MUL vs repeated ADD: {A_VAL} * {B_VAL} = {expected_product}"
6617
+
6618
+
6619
+ def div_via_repeated_sub(mem_size: int = 256) -> ProgramResult:
6620
+ """Compute floor(A/B) and (A mod B) by repeated subtraction."""
6621
+ A_VAL = 100
6622
+ B_VAL = 7
6623
+ expected_q = A_VAL // B_VAL # 14
6624
+ expected_r = A_VAL % B_VAL # 2
6625
+
6626
+ a = Asm(mem_size)
6627
+
6628
+ a.org(0)
6629
+ # ---- Repeated-subtraction division ----
6630
+ a.load(0, "A") # R0 = A (will become remainder)
6631
+ a.load(1, "B") # R1 = B (divisor)
6632
+ a.load(2, "ZERO") # R2 = 0 (will become quotient)
6633
+ a.load(3, "ONE") # R3 = 1 (increment)
6634
+
6635
+ a.label("loop")
6636
+ a.cmp(0, 1) # CMP R0, R1; carry=1 (no-borrow) iff R0 >= R1
6637
+ a.jnc("done") # if R0 < R1 (carry=0), exit loop
6638
+ a.sub(0, 1) # R0 -= B
6639
+ a.add(2, 3) # quotient += 1
6640
+ a.jmp("loop")
6641
+
6642
+ a.label("done")
6643
+ a.store(2, "OUT_Q_RPT") # quotient via repeated sub
6644
+ a.store(0, "OUT_R_RPT") # remainder via repeated sub
6645
+
6646
+ # ---- Direct DIV opcode for cross-check ----
6647
+ a.load(0, "A")
6648
+ a.load(1, "B")
6649
+ a.dw(_enc(0x8, 0, 1, 0)) # DIV R0, R1 -> R0 = R0 / R1 (8-bit DIV)
6650
+ a.store(0, "OUT_Q_DIV")
6651
+ a.halt()
6652
+
6653
+ a.org(0x80)
6654
+ a.label("A"); a.db(A_VAL)
6655
+ a.label("B"); a.db(B_VAL)
6656
+ a.label("ZERO"); a.db(0)
6657
+ a.label("ONE"); a.db(1)
6658
+ a.label("OUT_Q_RPT"); a.db(0)
6659
+ a.label("OUT_R_RPT"); a.db(0)
6660
+ a.label("OUT_Q_DIV"); a.db(0)
6661
+
6662
+ mem = a.assemble()
6663
+ expected = {
6664
+ a.labels["OUT_Q_RPT"]: expected_q,
6665
+ a.labels["OUT_R_RPT"]: expected_r,
6666
+ a.labels["OUT_Q_DIV"]: expected_q,
6667
+ }
6668
+ return mem, expected, 4 * (A_VAL // B_VAL + 4) + 12, (
6669
+ f"{A_VAL} / {B_VAL}: quotient {expected_q} (repeated SUB) "
6670
+ f"matches DIV opcode result; remainder {expected_r}"
6671
+ )
6672
+
6673
+
6674
+ def bitwise_chain(mem_size: int = 256) -> ProgramResult:
6675
+ """Run a chain of bitwise ops and verify each intermediate value."""
6676
+ A = 0xCC # 11001100
6677
+ B = 0xF0 # 11110000
6678
+ C = 0x0F # 00001111
6679
+ D = 0xAA # 10101010
6680
+
6681
+ s1 = A & B # 0xC0
6682
+ s2 = s1 | C # 0xCF
6683
+ s3 = s2 ^ D # 0x65
6684
+ s4 = (s3 << 1) & 0xFF # 0xCA
6685
+ s5 = s4 >> 1 # 0x65
6686
+
6687
+ a = Asm(mem_size)
6688
+ a.org(0)
6689
+ a.load(0, "A"); a.load(1, "B"); a.and_(0, 1); a.store(0, "S1")
6690
+ a.load(1, "C"); a.or_(0, 1); a.store(0, "S2")
6691
+ a.load(1, "D"); a.xor(0, 1); a.store(0, "S3")
6692
+ a.shl(0); a.store(0, "S4")
6693
+ a.shr(0); a.store(0, "S5")
6694
+ a.halt()
6695
+
6696
+ a.org(0x80)
6697
+ a.label("A"); a.db(A)
6698
+ a.label("B"); a.db(B)
6699
+ a.label("C"); a.db(C)
6700
+ a.label("D"); a.db(D)
6701
+ a.label("S1"); a.db(0)
6702
+ a.label("S2"); a.db(0)
6703
+ a.label("S3"); a.db(0)
6704
+ a.label("S4"); a.db(0)
6705
+ a.label("S5"); a.db(0)
6706
+
6707
+ mem = a.assemble()
6708
+ expected = {
6709
+ a.labels["S1"]: s1,
6710
+ a.labels["S2"]: s2,
6711
+ a.labels["S3"]: s3,
6712
+ a.labels["S4"]: s4,
6713
+ a.labels["S5"]: s5,
6714
+ }
6715
+ return mem, expected, 30, (
6716
+ f"bitwise chain AND/OR/XOR/SHL/SHR -> {s1:#x},{s2:#x},{s3:#x},{s4:#x},{s5:#x}"
6717
+ )
6718
+
6719
+
6720
+ def flags_policy(mem_size: int = 256) -> ProgramResult:
6721
+ """Only ADD, SUB, MUL and CMP write FLAGS; every other instruction leaves them."""
6722
+ a = Asm(mem_size)
6723
+
6724
+ a.org(0)
6725
+ # ----- Probe 1: Z=1 survives AND (nonzero result) -----
6726
+ a.load(0, "v5"); a.load(1, "v5"); a.cmp(0, 1) # Z=1
6727
+ a.load(2, "v3"); a.load(3, "v1"); a.and_(2, 3) # R2 = 3 & 1 = 1
6728
+ a.jz("p1_ok")
6729
+ a.load(2, "m_fail"); a.store(2, "out1"); a.jmp("p2")
6730
+ a.label("p1_ok")
6731
+ a.load(2, "m_b1"); a.store(2, "out1")
6732
+
6733
+ # ----- Probe 2: Z=0 survives XOR (zero result) -----
6734
+ a.label("p2")
6735
+ a.load(0, "v5"); a.load(1, "v3"); a.cmp(0, 1) # Z=0
6736
+ a.xor(2, 2) # R2 = 0
6737
+ a.jz("p2_fail")
6738
+ a.load(2, "m_b2"); a.store(2, "out2"); a.jmp("p3")
6739
+ a.label("p2_fail")
6740
+ a.load(2, "m_fail"); a.store(2, "out2")
6741
+
6742
+ # ----- Probe 3: C=1 survives SHR -----
6743
+ a.label("p3")
6744
+ a.load(0, "v255"); a.load(1, "v1"); a.add(0, 1) # C=1
6745
+ a.load(2, "v3"); a.shr(2) # R2 = 1
6746
+ a.jc("p3_ok")
6747
+ a.load(2, "m_fail"); a.store(2, "out3"); a.jmp("done")
6748
+ a.label("p3_ok")
6749
+ a.load(2, "m_b3"); a.store(2, "out3")
6750
+ a.label("done")
6751
+ a.halt()
6752
+
6753
+ a.org(0xA0)
6754
+ a.label("v1"); a.db(1)
6755
+ a.label("v3"); a.db(3)
6756
+ a.label("v5"); a.db(5)
6757
+ a.label("v255"); a.db(255)
6758
+ a.label("m_b1"); a.db(0xB1)
6759
+ a.label("m_b2"); a.db(0xB2)
6760
+ a.label("m_b3"); a.db(0xB3)
6761
+ a.label("m_fail"); a.db(0xEE)
6762
+ a.label("out1"); a.db(0)
6763
+ a.label("out2"); a.db(0)
6764
+ a.label("out3"); a.db(0)
6765
+
6766
+ mem = a.assemble()
6767
+ expected = {
6768
+ a.labels["out1"]: 0xB1,
6769
+ a.labels["out2"]: 0xB2,
6770
+ a.labels["out3"]: 0xB3,
6771
+ }
6772
+ return mem, expected, 60, "flag policy: AND/XOR/SHR leave FLAGS unchanged"
6773
+
6774
+
6775
+ SUITE = [
6776
+ ("fib", lambda mem_size: fib(11, mem_size)),
6777
+ ("sum_n", lambda mem_size: sum_n(10, mem_size)),
6778
+ ("self_mod_jmp", lambda mem_size: self_mod_jmp(mem_size)),
6779
+ ("all_branches", lambda mem_size: all_branches(mem_size)),
6780
+ ("call_pushes_pc", lambda mem_size: call_pushes_pc(mem_size)),
6781
+ ("bubble_sort_4", lambda mem_size: bubble_sort_4(mem_size)),
6782
+ ("cross_check_mul", lambda mem_size: cross_check_mul(mem_size)),
6783
+ ("div_via_repeated_sub", lambda mem_size: div_via_repeated_sub(mem_size)),
6784
+ ("bitwise_chain", lambda mem_size: bitwise_chain(mem_size)),
6785
+ ("flags_policy", lambda mem_size: flags_policy(mem_size)),
6786
+ ]
6787
+
6788
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # this file lives in tools/
6789
+
6790
+
6791
+ def load_tensors(path: Path):
6792
+ out = {}
6793
+ with safe_open(str(path), framework="pt") as f:
6794
+ for name in f.keys():
6795
+ out[name] = f.get_tensor(name).float()
6796
+ return out
6797
+
6798
+
6799
+ def run_program(cpu: GenericThresholdCPU, mem, max_cycles: int):
6800
+ addr_mask = (1 << cpu.addr_bits) - 1
6801
+ state = {
6802
+ "pc": 0,
6803
+ "regs": [0] * 4,
6804
+ "flags": [0] * 4,
6805
+ "mem": list(mem),
6806
+ "halted": False,
6807
+ "sp": addr_mask,
6808
+ }
6809
+ t0 = time.perf_counter()
6810
+ final, cycles = cpu.run(state, max_cycles=max_cycles)
6811
+ return final, cycles, time.perf_counter() - t0
6812
+
6813
+
6814
+ def check_expected(final, expected: dict) -> tuple[bool, list[str]]:
6815
+ failures = []
6816
+ for addr, want in expected.items():
6817
+ got = final["mem"][addr]
6818
+ if got != want:
6819
+ failures.append(f"M[0x{addr:04X}] = {got} (expected {want})")
6820
+ return (len(failures) == 0), failures
6821
+
6822
+
6823
+ def main_cpu() -> int:
6824
+ parser = argparse.ArgumentParser(description="Run the CPU program suite")
6825
+ parser.add_argument(
6826
+ "--model", type=str,
6827
+ default=os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
6828
+ help="Path to .safetensors variant",
6829
+ )
6830
+ parser.add_argument(
6831
+ "--only", type=str, default="",
6832
+ help="Comma-separated subset of program names to run",
6833
+ )
6834
+ args = parser.parse_args()
6835
+
6836
+ print(f"Loading {args.model}")
6837
+ tensors = load_tensors(Path(args.model))
6838
+ manifest = get_manifest(tensors)
6839
+ print(f"Manifest: data={manifest['data_bits']}-bit, addr={manifest['addr_bits']}-bit, "
6840
+ f"mem={manifest['memory_bytes']}B")
6841
+
6842
+ if manifest["memory_bytes"] < 256:
6843
+ print(f"ERROR: variant has {manifest['memory_bytes']}B memory; "
6844
+ f"the suite needs at least 256B (scratchpad).")
6845
+ return 2
6846
+
6847
+ cpu = GenericThresholdCPU(tensors)
6848
+ only = set(s.strip() for s in args.only.split(",") if s.strip())
6849
+
6850
+ print()
6851
+ print("=" * 80)
6852
+ print(f" CPU PROGRAM SUITE ({manifest['memory_bytes']}B mem, {manifest['data_bits']}-bit ALU)")
6853
+ print("=" * 80)
6854
+
6855
+ pass_count = 0
6856
+ fail_count = 0
6857
+ skip_count = 0
6858
+
6859
+ for name, builder in SUITE:
6860
+ if only and name not in only:
6861
+ skip_count += 1
6862
+ continue
6863
+ try:
6864
+ mem, expected, max_cycles, desc = builder(manifest["memory_bytes"])
6865
+ except Exception as e:
6866
+ print(f" {name:18} BUILD ERROR: {e}")
6867
+ fail_count += 1
6868
+ continue
6869
+
6870
+ if len(mem) != manifest["memory_bytes"]:
6871
+ print(f" {name:18} SKIP (program built {len(mem)}B, "
6872
+ f"variant has {manifest['memory_bytes']}B)")
6873
+ skip_count += 1
6874
+ continue
6875
+
6876
+ final, cycles, elapsed = run_program(cpu, mem, max_cycles)
6877
+ ok, failures = check_expected(final, expected)
6878
+ if ok and not final["halted"]:
6879
+ ok = False
6880
+ failures.append(f"did not HALT within {max_cycles} cycles")
6881
+
6882
+ status = "PASS" if ok else "FAIL"
6883
+ if ok:
6884
+ pass_count += 1
6885
+ else:
6886
+ fail_count += 1
6887
+ print(f" {name:18} {status} ({cycles:>3} cyc, {elapsed:>5.2f}s) {desc}")
6888
+ if not ok:
6889
+ for f in failures[:6]:
6890
+ print(f" - {f}")
6891
+
6892
+ print()
6893
+ print("=" * 80)
6894
+ total = pass_count + fail_count
6895
+ print(f" PASS: {pass_count}/{total} FAIL: {fail_count}/{total} SKIP: {skip_count}")
6896
+ return 0 if fail_count == 0 else 1
6897
+
6898
+
6899
+ COMMANDS = {'eval': main, 'variants': main_all, 'cpu': main_cpu}
6900
+
6901
+
6902
+ if __name__ == "__main__":
6903
+ if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
6904
+ sys.exit(COMMANDS['eval']())
6905
+ cmd = sys.argv.pop(1)
6906
+ sys.argv[0] += ' ' + cmd
6907
+ sys.exit(COMMANDS[cmd]())
src/eval_all.py DELETED
@@ -1,912 +0,0 @@
1
- """
2
- Unified evaluation harness for any threshold-computer variant.
3
-
4
- Drops the `--cpu-test` smoke test (which was hardcoded to 16-bit/64KB) and
5
- adds variant-aware sweep modes. The same harness handles every (data_bits,
6
- addr_bits) configuration: it reads the manifest from each safetensors file,
7
- runs the BatchedFitnessEvaluator at the right device, and reports per-file
8
- plus per-category results.
9
-
10
- Usage:
11
- python eval_all.py path/to/file.safetensors # one file
12
- python eval_all.py variants/ # every .safetensors in dir
13
- python eval_all.py --device cpu variants/ # CPU only (default)
14
- python eval_all.py --pop_size 32 variants/ # batched pop eval
15
- python eval_all.py --debug path/to/file.safetensors # per-circuit detail
16
- python eval_all.py --cpu-program PATH # also run an assembled program
17
- # through the threshold CPU
18
- # sized to the file's manifest
19
-
20
- Exit code:
21
- 0 if all files PASS (fitness >= 0.9999)
22
- N where N is the number of FAILing files
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import argparse
28
- import json
29
- import os
30
- import sys
31
- import time
32
- from pathlib import Path
33
- from typing import Any, Dict, List, Optional, Tuple
34
-
35
- import torch
36
- from safetensors import safe_open
37
-
38
- # Reuse eval.py's evaluator (variant-aware)
39
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
40
- from eval import (
41
- BatchedFitnessEvaluator,
42
- create_population,
43
- load_model,
44
- get_manifest,
45
- heaviside,
46
- int_to_bits,
47
- bits_to_int,
48
- bits_msb_to_lsb,
49
- )
50
-
51
-
52
- # ---------------------------------------------------------------------------
53
- # Variant-aware threshold ALU + CPU
54
- # ---------------------------------------------------------------------------
55
-
56
- class GenericThresholdALU:
57
- """Variant-aware threshold ALU walking the gates of a loaded variant.
58
-
59
- 8-bit primitives: add8/sub8/mul8/div8, packed bitwise and8/or8/xor8,
60
- shl8/shr8, and the bit-cascade comparators via cmp8. Width-generic
61
- add_n/sub_n/mul_n/cmp_n cover the 16- and 32-bit circuit families.
62
- """
63
-
64
- def __init__(self, tensors: Dict[str, torch.Tensor], data_bits: int):
65
- self.T = tensors
66
- self.data_bits = data_bits
67
-
68
- def _g(self, name, inputs):
69
- w = self.T[name + ".weight"].view(-1)
70
- b = self.T[name + ".bias"].view(-1)
71
- return int(heaviside((torch.tensor(inputs, dtype=torch.float32) * w).sum() + b).item())
72
-
73
- def _xor_or_nand(self, prefix, inputs):
74
- a, b_ = inputs
75
- h_or = self._g(f"{prefix}.layer1.or", [a, b_])
76
- h_nand = self._g(f"{prefix}.layer1.nand", [a, b_])
77
- return self._g(f"{prefix}.layer2", [h_or, h_nand])
78
-
79
- def _fa(self, prefix, a, b, cin):
80
- s1 = self._xor_or_nand(f"{prefix}.ha1.sum", [a, b])
81
- c1 = self._g(f"{prefix}.ha1.carry", [a, b])
82
- s2 = self._xor_or_nand(f"{prefix}.ha2.sum", [s1, cin])
83
- c2 = self._g(f"{prefix}.ha2.carry", [s1, cin])
84
- cout = self._g(f"{prefix}.carry_or", [c1, c2])
85
- return s2, cout
86
-
87
- def add8(self, a, b):
88
- a_lsb = list(reversed(int_to_bits(a, 8)))
89
- b_lsb = list(reversed(int_to_bits(b, 8)))
90
- carry = 0
91
- s_lsb = []
92
- for i in range(8):
93
- s, carry = self._fa(f"arithmetic.ripplecarry8bit.fa{i}", a_lsb[i], b_lsb[i], carry)
94
- s_lsb.append(s)
95
- return bits_to_int(list(reversed(s_lsb))), carry
96
-
97
- def sub8(self, a, b):
98
- a_lsb = list(reversed(int_to_bits(a, 8)))
99
- b_lsb = list(reversed(int_to_bits(b, 8)))
100
- carry = 1
101
- d_lsb = []
102
- for i in range(8):
103
- notb = self._g(f"arithmetic.sub8bit.notb{i}", [b_lsb[i]])
104
- x1 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor1", [a_lsb[i], notb])
105
- x2 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor2", [x1, carry])
106
- and1 = self._g(f"arithmetic.sub8bit.fa{i}.and1", [a_lsb[i], notb])
107
- and2 = self._g(f"arithmetic.sub8bit.fa{i}.and2", [x1, carry])
108
- carry = self._g(f"arithmetic.sub8bit.fa{i}.or_carry", [and1, and2])
109
- d_lsb.append(x2)
110
- return bits_to_int(list(reversed(d_lsb))), carry
111
-
112
- _CMP_KIND = {"greaterthan": "gt", "lessthan": "lt", "eq": "eq", "equality": "eq",
113
- "greaterorequal": "ge", "lessorequal": "le"}
114
-
115
- def _cmp_bit_cascade(self, cmp_prefix: str, finals: Dict[str, str],
116
- a_bits_msb: List[int], b_bits_msb: List[int], kind: str) -> int:
117
- """Walk an add_bit_cascade_compare structure (bit 0 is the MSB)."""
118
- bits = len(a_bits_msb)
119
- bit_gt, bit_lt, bit_eq = [], [], []
120
- for i in range(bits):
121
- ab = [a_bits_msb[i], b_bits_msb[i]]
122
- bit_gt.append(self._g(f"{cmp_prefix}.bit{i}.gt", ab))
123
- bit_lt.append(self._g(f"{cmp_prefix}.bit{i}.lt", ab))
124
- eq_and = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.and", ab)
125
- eq_nor = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.nor", ab)
126
- bit_eq.append(self._g(f"{cmp_prefix}.bit{i}.eq", [eq_and, eq_nor]))
127
- cas_gt, cas_lt = [bit_gt[0]], [bit_lt[0]]
128
- for i in range(1, bits):
129
- eq_pref = self._g(f"{cmp_prefix}.cascade.eq_prefix.bit{i}", bit_eq[:i])
130
- cas_gt.append(self._g(f"{cmp_prefix}.cascade.gt.bit{i}", [eq_pref, bit_gt[i]]))
131
- cas_lt.append(self._g(f"{cmp_prefix}.cascade.lt.bit{i}", [eq_pref, bit_lt[i]]))
132
- if kind == "gt":
133
- return self._g(finals["gt"], cas_gt)
134
- if kind == "lt":
135
- return self._g(finals["lt"], cas_lt)
136
- if kind == "eq":
137
- return self._g(finals["eq"], bit_eq)
138
- if kind == "ge":
139
- lt = self._g(finals["lt"], cas_lt)
140
- not_lt = self._g(finals["ge"] + ".not_lt", [lt])
141
- return self._g(finals["ge"], [not_lt])
142
- if kind == "le":
143
- gt = self._g(finals["gt"], cas_gt)
144
- not_gt = self._g(finals["le"] + ".not_gt", [gt])
145
- return self._g(finals["le"], [not_gt])
146
- raise ValueError(kind)
147
-
148
- def cmp8(self, a, b, kind):
149
- return self.cmp_n(a, b, kind, 8)
150
-
151
- def mul8(self, a, b):
152
- ab = int_to_bits(a, 8)
153
- bb = int_to_bits(b, 8)
154
- result = 0
155
- for j in range(8):
156
- if bb[j] == 0:
157
- continue
158
- row = 0
159
- for i in range(8):
160
- pp = self._g(f"alu.alu8bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
161
- row |= (pp << (7 - i))
162
- shift = 7 - j
163
- result, _ = self.add8(result & 0xFF, (row << shift) & 0xFF)
164
- return result & 0xFF
165
-
166
- def _packed_pair_op(self, name: str, a: int, b: int) -> int:
167
- """8 parallel 2-input gates stored packed: weight [16], bias [8]."""
168
- a_bits = int_to_bits(a, 8)
169
- b_bits = int_to_bits(b, 8)
170
- w = self.T[f"{name}.weight"].view(-1)
171
- bias = self.T[f"{name}.bias"].view(-1)
172
- out = []
173
- for bit in range(8):
174
- inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
175
- out.append(int(heaviside((inp * w[bit * 2:bit * 2 + 2]).sum() + bias[bit]).item()))
176
- return bits_to_int(out)
177
-
178
- def and8(self, a, b):
179
- return self._packed_pair_op("alu.alu8bit.and", a, b)
180
-
181
- def or8(self, a, b):
182
- return self._packed_pair_op("alu.alu8bit.or", a, b)
183
-
184
- def xor8(self, a, b):
185
- a_bits = int_to_bits(a, 8)
186
- b_bits = int_to_bits(b, 8)
187
- w_or = self.T["alu.alu8bit.xor.layer1.or.weight"].view(-1)
188
- b_or = self.T["alu.alu8bit.xor.layer1.or.bias"].view(-1)
189
- w_nand = self.T["alu.alu8bit.xor.layer1.nand.weight"].view(-1)
190
- b_nand = self.T["alu.alu8bit.xor.layer1.nand.bias"].view(-1)
191
- w2 = self.T["alu.alu8bit.xor.layer2.weight"].view(-1)
192
- b2 = self.T["alu.alu8bit.xor.layer2.bias"].view(-1)
193
- out = []
194
- for bit in range(8):
195
- inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
196
- h_or = heaviside((inp * w_or[bit * 2:bit * 2 + 2]).sum() + b_or[bit])
197
- h_nand = heaviside((inp * w_nand[bit * 2:bit * 2 + 2]).sum() + b_nand[bit])
198
- hidden = torch.stack([h_or, h_nand])
199
- out.append(int(heaviside((hidden * w2[bit * 2:bit * 2 + 2]).sum() + b2[bit]).item()))
200
- return bits_to_int(out)
201
-
202
- def shl8(self, a):
203
- a_bits = int_to_bits(a, 8)
204
- out = []
205
- for bit in range(8):
206
- src = float(a_bits[bit + 1]) if bit < 7 else 0.0
207
- out.append(self._g(f"alu.alu8bit.shl.bit{bit}", [src]))
208
- return bits_to_int(out)
209
-
210
- def shr8(self, a):
211
- a_bits = int_to_bits(a, 8)
212
- out = []
213
- for bit in range(8):
214
- src = float(a_bits[bit - 1]) if bit > 0 else 0.0
215
- out.append(self._g(f"alu.alu8bit.shr.bit{bit}", [src]))
216
- return bits_to_int(out)
217
-
218
- def div8(self, a, b):
219
- """8-bit restoring division through the per-stage bit-cascade GE
220
- comparators and the sub8 subtractor. Returns (quotient, remainder);
221
- divide-by-zero yields (0xFF, a)."""
222
- if b == 0:
223
- return 0xFF, a
224
- a_bits = int_to_bits(a, 8)
225
- div_bits = int_to_bits(b, 8)
226
- quotient = 0
227
- remainder = 0
228
- for stage in range(8):
229
- remainder = ((remainder << 1) | a_bits[stage]) & 0xFF
230
- prefix = f"alu.alu8bit.div.stage{stage}"
231
- finals = {
232
- "gt": f"{prefix}.cmp_bc.gt",
233
- "lt": f"{prefix}.cmp_bc.lt",
234
- "eq": f"{prefix}.cmp_bc.eq",
235
- "ge": f"{prefix}.cmp",
236
- "le": f"{prefix}.cmp_bc.le",
237
- }
238
- ge = self._cmp_bit_cascade(f"{prefix}.cmp_bc", finals,
239
- int_to_bits(remainder, 8), div_bits, "ge")
240
- if ge:
241
- remainder, _ = self.sub8(remainder, b)
242
- quotient = (quotient << 1) | 1
243
- else:
244
- quotient = quotient << 1
245
- return quotient & 0xFF, remainder & 0xFF
246
-
247
- # ----- N-bit primitives (for 16-bit and 32-bit variants) ----------------
248
-
249
- def add_n(self, a: int, b: int, bits: int):
250
- """Width-generic ripple-carry add via arithmetic.ripplecarry{N}bit."""
251
- prefix = f"arithmetic.ripplecarry{bits}bit"
252
- a_lsb = list(reversed(int_to_bits(a, bits)))
253
- b_lsb = list(reversed(int_to_bits(b, bits)))
254
- carry = 0
255
- s_lsb = []
256
- for i in range(bits):
257
- s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], b_lsb[i], carry)
258
- s_lsb.append(s)
259
- return bits_to_int(list(reversed(s_lsb))), carry
260
-
261
- def sub_n(self, a: int, b: int, bits: int):
262
- """N-bit two's-complement subtract via arithmetic.sub{N}bit (N >= 16).
263
-
264
- Structure (per build.add_sub_nbits): N NOT gates + N standard full adders.
265
- """
266
- prefix = f"arithmetic.sub{bits}bit"
267
- a_lsb = list(reversed(int_to_bits(a, bits)))
268
- b_lsb = list(reversed(int_to_bits(b, bits)))
269
- # NOT each B bit
270
- notb = [self._g(f"{prefix}.not_b.bit{i}", [b_lsb[i]]) for i in range(bits)]
271
- carry = 1 # carry-in = 1 for two's-complement
272
- d_lsb = []
273
- for i in range(bits):
274
- s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], notb[i], carry)
275
- d_lsb.append(s)
276
- return bits_to_int(list(reversed(d_lsb))), carry
277
-
278
- def cmp_n(self, a: int, b: int, kind: str, bits: int):
279
- """N-bit unsigned comparison via the bit-cascade comparator family
280
- (arithmetic.cmp{N}bit.* per build.add_bit_cascade_compare)."""
281
- short = self._CMP_KIND[kind]
282
- finals = {
283
- "gt": f"arithmetic.greaterthan{bits}bit",
284
- "lt": f"arithmetic.lessthan{bits}bit",
285
- "eq": f"arithmetic.equality{bits}bit",
286
- "ge": f"arithmetic.greaterorequal{bits}bit",
287
- "le": f"arithmetic.lessorequal{bits}bit",
288
- }
289
- return self._cmp_bit_cascade(f"arithmetic.cmp{bits}bit", finals,
290
- int_to_bits(a, bits), int_to_bits(b, bits), short)
291
-
292
- def mul_n(self, a: int, b: int, bits: int):
293
- """N-bit shift-add multiply (low N bits only)."""
294
- ab = int_to_bits(a, bits)
295
- bb = int_to_bits(b, bits)
296
- mask = (1 << bits) - 1
297
- result = 0
298
- for j in range(bits):
299
- if bb[j] == 0:
300
- continue
301
- row = 0
302
- for i in range(bits):
303
- pp = self._g(f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
304
- row |= (pp << (bits - 1 - i))
305
- shift = (bits - 1) - j
306
- result, _ = self.add_n(result & mask, (row << shift) & mask, bits)
307
- return result & mask
308
-
309
-
310
- class GenericThresholdCPU:
311
- """Variant-aware CPU runtime. Sized from the variant's manifest."""
312
-
313
- def __init__(self, tensors: Dict[str, torch.Tensor]):
314
- self.T = tensors
315
- m = get_manifest(tensors)
316
- self.data_bits = m["data_bits"]
317
- self.addr_bits = m["addr_bits"]
318
- self.mem_bytes = m["memory_bytes"]
319
- # 8-bit CPU primitives (ripplecarry8bit, sub8bit, alu.alu8bit.*, memory.*,
320
- # control.*) are present in every variant regardless of manifest data_bits.
321
- # Wider data widths simply add additional standalone ALU primitives.
322
- if self.mem_bytes == 0:
323
- raise NotImplementedError(
324
- "Pure-ALU variants have no memory; cannot run CPU programs"
325
- )
326
- self.alu = GenericThresholdALU(tensors, 8)
327
-
328
- def _addr_decode(self, addr):
329
- bits = torch.tensor(int_to_bits(addr, self.addr_bits), dtype=torch.float32)
330
- w = self.T["memory.addr_decode.weight"]
331
- b = self.T["memory.addr_decode.bias"]
332
- return heaviside((w * bits).sum(dim=1) + b)
333
-
334
- def mem_read(self, mem, addr):
335
- sel = self._addr_decode(addr)
336
- mem_bits = torch.tensor(
337
- [int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
338
- )
339
- and_w = self.T["memory.read.and.weight"]
340
- and_b = self.T["memory.read.and.bias"]
341
- or_w = self.T["memory.read.or.weight"]
342
- or_b = self.T["memory.read.or.bias"]
343
- out = []
344
- for bit in range(8):
345
- inp = torch.stack([mem_bits[:, bit], sel], dim=1)
346
- and_out = heaviside((inp * and_w[bit]).sum(dim=1) + and_b[bit])
347
- out.append(int(heaviside((and_out * or_w[bit]).sum() + or_b[bit]).item()))
348
- return bits_to_int(out)
349
-
350
- def mem_write(self, mem, addr, value):
351
- sel = self._addr_decode(addr)
352
- data_bits = torch.tensor(int_to_bits(value, 8), dtype=torch.float32)
353
- mem_bits = torch.tensor(
354
- [int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
355
- )
356
- sel_w = self.T["memory.write.sel.weight"]
357
- sel_b = self.T["memory.write.sel.bias"]
358
- nsel_w = self.T["memory.write.nsel.weight"].squeeze(1)
359
- nsel_b = self.T["memory.write.nsel.bias"]
360
- and_old_w = self.T["memory.write.and_old.weight"]
361
- and_old_b = self.T["memory.write.and_old.bias"]
362
- and_new_w = self.T["memory.write.and_new.weight"]
363
- and_new_b = self.T["memory.write.and_new.bias"]
364
- or_w = self.T["memory.write.or.weight"]
365
- or_b = self.T["memory.write.or.bias"]
366
- we = torch.ones_like(sel)
367
- sel_inp = torch.stack([sel, we], dim=1)
368
- write_sel = heaviside((sel_inp * sel_w).sum(dim=1) + sel_b)
369
- nsel = heaviside(write_sel * nsel_w + nsel_b)
370
- for bit in range(8):
371
- old = mem_bits[:, bit]
372
- data_bit = data_bits[bit].expand(self.mem_bytes)
373
- inp_old = torch.stack([old, nsel], dim=1)
374
- inp_new = torch.stack([data_bit, write_sel], dim=1)
375
- and_old = heaviside((inp_old * and_old_w[:, bit]).sum(dim=1) + and_old_b[:, bit])
376
- and_new = heaviside((inp_new * and_new_w[:, bit]).sum(dim=1) + and_new_b[:, bit])
377
- or_inp = torch.stack([and_old, and_new], dim=1)
378
- new_bit = heaviside((or_inp * or_w[:, bit]).sum(dim=1) + or_b[:, bit])
379
- mem_bits[:, bit] = new_bit
380
- return [bits_to_int([int(b) for b in mem_bits[i].tolist()]) for i in range(self.mem_bytes)]
381
-
382
- # Conditions 0..7 map to (mux circuit, flag index into [Z, N, C, V]).
383
- # Odd conditions are the negated forms; the mux select is the complemented
384
- # flag (the circuits are plain per-bit muxes -- see control.* .inputs,
385
- # which name the select $not_zero / $not_carry / ... for the odd forms).
386
- _JCC = [("control.jz", 0), ("control.jnz", 0), ("control.jc", 2), ("control.jnc", 2),
387
- ("control.jn", 1), ("control.jp", 1), ("control.jv", 3), ("control.jnv", 3)]
388
-
389
- def _jcc_pc(self, circuit: str, pc: int, target: int, sel: int) -> int:
390
- """Per-bit 2:1 mux over the PC: sel ? target : pc, addr_bits wide."""
391
- pc_bits = int_to_bits(pc, self.addr_bits)
392
- t_bits = int_to_bits(target, self.addr_bits)
393
- out = []
394
- for bit in range(self.addr_bits):
395
- bp = f"{circuit}.bit{bit}"
396
- not_sel = self.alu._g(f"{bp}.not_sel", [float(sel)])
397
- and_a = self.alu._g(f"{bp}.and_a", [float(pc_bits[bit]), not_sel])
398
- and_b = self.alu._g(f"{bp}.and_b", [float(t_bits[bit]), float(sel)])
399
- out.append(self.alu._g(f"{bp}.or", [and_a, and_b]))
400
- return bits_to_int(out)
401
-
402
- def _sp_dec(self, sp: int) -> int:
403
- """SP - 1 through the control.push.sp_dec borrow chain. Gate bit index
404
- is MSB-first (bit addr_bits-1 is the LSB); the bit complement feeding
405
- each borrow AND is fixed wiring, as in the gate fitness suite."""
406
- bits = int_to_bits(sp, self.addr_bits)
407
- out = [0] * self.addr_bits
408
- borrow = 1
409
- for bit in range(self.addr_bits - 1, -1, -1):
410
- bp = f"control.push.sp_dec.bit{bit}"
411
- h_or = self.alu._g(f"{bp}.xor.layer1.or", [float(bits[bit]), float(borrow)])
412
- h_nand = self.alu._g(f"{bp}.xor.layer1.nand", [float(bits[bit]), float(borrow)])
413
- out[bit] = self.alu._g(f"{bp}.xor.layer2", [h_or, h_nand])
414
- borrow = self.alu._g(f"{bp}.borrow", [float(1 - bits[bit]), float(borrow)])
415
- return bits_to_int(out)
416
-
417
- def _decode_op(self, opcode):
418
- """4-to-16 opcode one-hot through the gate decoder."""
419
- ob = [float((opcode >> j) & 1) for j in range(4)]
420
- return [self.alu._g(f"control.decode.op{n}", ob) for n in range(16)]
421
-
422
- def _inc(self, pc, name, start):
423
- """PC + 2^start through the gate increment chain (LSB-first)."""
424
- ab = self.addr_bits
425
- pcb = [(pc >> k) & 1 for k in range(ab)]
426
- out = [0] * ab
427
- carry = 0
428
- for k in range(ab):
429
- if k < start:
430
- out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}", [float(pcb[k])])
431
- else:
432
- cin = 1 if k == start else carry
433
- o = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.or", [float(pcb[k]), float(cin)])
434
- nd = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.nand", [float(pcb[k]), float(cin)])
435
- out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer2", [o, nd])
436
- carry = self.alu._g(f"control.pcnext.{name}.bit{k}.carry", [float(pcb[k]), float(cin)])
437
- return sum(out[k] << k for k in range(ab))
438
-
439
- def _pcnext(self, pc2, pc4, addr, jcc, use_pc4, use_addr, use_jcc):
440
- """Next-PC priority mux through the gates: use_addr ? addr :
441
- (use_jcc ? jcc : (use_pc4 ? pc4 : pc2))."""
442
- ab = self.addr_bits
443
- p2 = [(pc2 >> k) & 1 for k in range(ab)]
444
- p4 = [(pc4 >> k) & 1 for k in range(ab)]
445
- aB = [(addr >> k) & 1 for k in range(ab)]
446
- jB = [(jcc >> k) & 1 for k in range(ab)]
447
- out = [0] * ab
448
- for k in range(ab):
449
- ns = self.alu._g(f"control.pcnext.m_pc4.bit{k}.not_sel", [float(use_pc4)])
450
- aa = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_a", [float(p2[k]), ns])
451
- bb = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_b", [float(p4[k]), float(use_pc4)])
452
- m1 = self.alu._g(f"control.pcnext.m_pc4.bit{k}.or", [aa, bb])
453
- ns = self.alu._g(f"control.pcnext.m_jcc.bit{k}.not_sel", [float(use_jcc)])
454
- aa = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_a", [float(m1), ns])
455
- bb = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_b", [float(jB[k]), float(use_jcc)])
456
- m2 = self.alu._g(f"control.pcnext.m_jcc.bit{k}.or", [aa, bb])
457
- ns = self.alu._g(f"control.pcnext.m_addr.bit{k}.not_sel", [float(use_addr)])
458
- aa = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_a", [float(m2), ns])
459
- bb = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_b", [float(aB[k]), float(use_addr)])
460
- out[k] = self.alu._g(f"control.pcnext.m_addr.bit{k}.or", [aa, bb])
461
- return sum(out[k] << k for k in range(ab))
462
-
463
- def step(self, state):
464
- if state["halted"]:
465
- return state
466
- s = dict(state)
467
- s["mem"] = state["mem"][:]
468
- s["regs"] = state["regs"][:]
469
- s["flags"] = state["flags"][:]
470
- addr_mask = (1 << self.addr_bits) - 1
471
- pc = s["pc"]
472
- hi = self.mem_read(s["mem"], pc & addr_mask)
473
- lo = self.mem_read(s["mem"], (pc + 1) & addr_mask)
474
- ir = ((hi & 0xFF) << 8) | (lo & 0xFF)
475
- opcode = (ir >> 12) & 0xF
476
- oh = self._decode_op(opcode) # gate opcode decode (one-hots)
477
- rd = (ir >> 10) & 0x3
478
- rs = (ir >> 8) & 0x3
479
- imm = ir & 0xFF
480
- addr = None
481
- if oh[0xA] or oh[0xB] or oh[0xC] or oh[0xD] or oh[0xE]:
482
- ah = self.mem_read(s["mem"], (pc + 2) & addr_mask)
483
- al = self.mem_read(s["mem"], (pc + 3) & addr_mask)
484
- addr = (((ah & 0xFF) << 8) | (al & 0xFF)) & addr_mask
485
- pc2 = self._inc(pc, "pc2", 1) # PC+2 through the gate chain
486
- pc4 = self._inc(pc, "pc4", 2) # PC+4 through the gate chain
487
- a = s["regs"][rd]
488
- b = s["regs"][rs]
489
- result = a
490
- carry = 0
491
- overflow = 0
492
- write_result = True
493
- jcc_val = 0
494
- if oh[0x0]:
495
- result, carry = self.alu.add8(a, b)
496
- overflow = 1 if (((a ^ result) & (b ^ result)) & 0x80) else 0
497
- elif oh[0x1]:
498
- result, carry = self.alu.sub8(a, b)
499
- overflow = 1 if (((a ^ b) & (a ^ result)) & 0x80) else 0
500
- elif oh[0x2]:
501
- result = self.alu.and8(a, b)
502
- elif oh[0x3]:
503
- result = self.alu.or8(a, b)
504
- elif oh[0x4]:
505
- result = self.alu.xor8(a, b)
506
- elif oh[0x5]:
507
- result = self.alu.shl8(a)
508
- elif oh[0x6]:
509
- result = self.alu.shr8(a)
510
- elif oh[0x7]:
511
- result = self.alu.mul8(a, b)
512
- elif oh[0x8]:
513
- result, _ = self.alu.div8(a, b)
514
- elif oh[0x9]: # CMP: set flags only
515
- r2, carry = self.alu.sub8(a, b)
516
- z = 1 if r2 == 0 else 0
517
- n = 1 if (r2 & 0x80) else 0
518
- v = 1 if (((a ^ b) & (a ^ r2)) & 0x80) else 0
519
- s["flags"] = [z, n, carry, v]
520
- write_result = False
521
- elif oh[0xA]: # LOAD
522
- result = self.mem_read(s["mem"], addr)
523
- elif oh[0xB]: # STORE
524
- s["mem"] = self.mem_write(s["mem"], addr, b & 0xFF)
525
- write_result = False
526
- elif oh[0xC]: # JMP (PC = addr via pcnext)
527
- write_result = False
528
- elif oh[0xD]: # JCC (PC via jcc mux + pcnext)
529
- cond = imm & 0x7
530
- circuit, flag_idx = self._JCC[cond]
531
- flag = s["flags"][flag_idx]
532
- sel = flag if cond % 2 == 0 else 1 - flag
533
- jcc_val = self._jcc_pc(circuit, pc4, addr, sel)
534
- write_result = False
535
- elif oh[0xE]: # CALL: push PC+4, PC = addr
536
- ret_addr = pc4 & 0xFFFF
537
- sp = s.get("sp", addr_mask)
538
- sp = self._sp_dec(sp)
539
- s["mem"] = self.mem_write(s["mem"], sp, (ret_addr >> 8) & 0xFF)
540
- sp = self._sp_dec(sp)
541
- s["mem"] = self.mem_write(s["mem"], sp, ret_addr & 0xFF)
542
- s["sp"] = sp
543
- write_result = False
544
- elif oh[0xF]: # HALT
545
- s["halted"] = True
546
- return s
547
-
548
- if write_result:
549
- s["regs"][rd] = result & 0xFF
550
- if oh[0x0] or oh[0x1] or oh[0x7]:
551
- z = 1 if (result & 0xFF) == 0 else 0
552
- n = 1 if (result & 0x80) else 0
553
- s["flags"] = [z, n, carry, overflow]
554
- # PC sequencing through the gate mux.
555
- use_pc4 = 1 if (oh[0xA] or oh[0xB]) else 0
556
- use_addr = 1 if (oh[0xC] or oh[0xE]) else 0
557
- s["pc"] = self._pcnext(pc2, pc4, addr or 0, jcc_val, use_pc4, use_addr, oh[0xD])
558
- return s
559
-
560
- def run(self, state, max_cycles=200):
561
- s = state
562
- cycles = 0
563
- while not s["halted"] and cycles < max_cycles:
564
- s = self.step(s)
565
- cycles += 1
566
- return s, cycles
567
-
568
-
569
- def _encode_instr(opcode, rd, rs, imm):
570
- return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm & 0xFF)
571
-
572
-
573
- def _w16(mem, addr, value):
574
- mem[addr] = (value >> 8) & 0xFF
575
- mem[addr + 1] = value & 0xFF
576
-
577
-
578
- PROGRAM_MIN_BYTES = 0x84 # code 0x00..0x1F + data 0x80..0x83
579
-
580
-
581
- def builtin_program(addr_bits: int) -> Tuple[List[int], int]:
582
- """Sum 5+4+3+2+1 via a loop. Returns (mem, expected_result_at_0x83).
583
-
584
- Compact layout: code at 0x00..0x1F (32 bytes), data at 0x80..0x83 (4 bytes).
585
- Total footprint 132 bytes -- fits within scratchpad (256 B) and larger.
586
- Requires addr_bits >= 8.
587
- """
588
- if (1 << addr_bits) < PROGRAM_MIN_BYTES:
589
- raise ValueError(f"addr_bits={addr_bits} too small for builtin program")
590
- mem = [0] * (1 << addr_bits)
591
- mem[0x80] = 5 # initial counter
592
- mem[0x81] = 1 # decrement
593
- mem[0x82] = 0 # zero (for compare and accumulator init)
594
- # mem[0x83] is the output
595
- _w16(mem, 0x0000, _encode_instr(0xA, 1, 0, 0)); _w16(mem, 0x0002, 0x0080)
596
- _w16(mem, 0x0004, _encode_instr(0xA, 2, 0, 0)); _w16(mem, 0x0006, 0x0081)
597
- _w16(mem, 0x0008, _encode_instr(0xA, 3, 0, 0)); _w16(mem, 0x000A, 0x0082)
598
- _w16(mem, 0x000C, _encode_instr(0xA, 0, 0, 0)); _w16(mem, 0x000E, 0x0082)
599
- _w16(mem, 0x0010, _encode_instr(0x0, 0, 1, 0))
600
- _w16(mem, 0x0012, _encode_instr(0x1, 1, 2, 0))
601
- _w16(mem, 0x0014, _encode_instr(0x9, 1, 3, 0))
602
- _w16(mem, 0x0016, _encode_instr(0xD, 0, 0, 0x01)); _w16(mem, 0x0018, 0x0010)
603
- _w16(mem, 0x001A, _encode_instr(0xB, 0, 0, 0)); _w16(mem, 0x001C, 0x0083)
604
- _w16(mem, 0x001E, _encode_instr(0xF, 0, 0, 0))
605
- return mem, 15
606
-
607
-
608
- # ---------------------------------------------------------------------------
609
- # Eval driver
610
- # ---------------------------------------------------------------------------
611
-
612
- def _file_fingerprint(path: Path) -> str:
613
- """Stable cache key for a safetensors file: sha256 of its content.
614
-
615
- Hashes are content-addressed so renaming a file doesn't blow the cache,
616
- but mtime-only would re-key on every clone of the repo. The sha256 of a
617
- 30 MB safetensors finishes in tens of milliseconds — small compared to
618
- a 5,900-test fitness run.
619
- """
620
- import hashlib
621
- h = hashlib.sha256()
622
- with open(path, "rb") as f:
623
- for chunk in iter(lambda: f.read(1 << 20), b""):
624
- h.update(chunk)
625
- return h.hexdigest()
626
-
627
-
628
- def _cache_key(path: Path, opts: Dict[str, Any]) -> str:
629
- """Cache key combining file content with the relevant evaluation options."""
630
- fp = _file_fingerprint(path)
631
- opt_str = json.dumps(opts, sort_keys=True)
632
- import hashlib
633
- suffix = hashlib.sha256(opt_str.encode("utf-8")).hexdigest()[:8]
634
- return f"{fp}_{suffix}"
635
-
636
-
637
- def _load_cache(cache_dir: Path, key: str) -> Dict[str, Any] | None:
638
- p = cache_dir / f"{key}.json"
639
- if not p.exists():
640
- return None
641
- try:
642
- return json.loads(p.read_text(encoding="utf-8"))
643
- except (json.JSONDecodeError, OSError):
644
- return None
645
-
646
-
647
- def _save_cache(cache_dir: Path, key: str, payload: Dict[str, Any]) -> None:
648
- cache_dir.mkdir(parents=True, exist_ok=True)
649
- p = cache_dir / f"{key}.json"
650
- try:
651
- p.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
652
- except OSError:
653
- pass
654
-
655
-
656
- def list_safetensors(path: Path) -> List[Path]:
657
- if path.is_file():
658
- return [path]
659
- if path.is_dir():
660
- return sorted(p for p in path.glob("*.safetensors") if p.is_file())
661
- return []
662
-
663
-
664
- # Standalone machines carry their own circuit inventory and are verified by
665
- # their own scripts, not the gate-fitness suite. Each maps to the tool that
666
- # checks it.
667
- MACHINE_VERIFIER = {
668
- "subleq8": "machines.py",
669
- "rv32": "machines.py",
670
- "matrix8": "matrix8.py",
671
- "subleq8io": "constructor8.py",
672
- "reflect": "reflect.py",
673
- "attractor": "tools/test_attractor.py",
674
- "reversible": "src/reversible.py",
675
- "ca": "src/ca.py",
676
- "tile": "src/tile.py",
677
- }
678
-
679
-
680
- def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_program: bool) -> Dict:
681
- out: Dict = {"path": str(path), "filename": path.name}
682
- # Skip standalone machines cleanly rather than error on missing standard gates.
683
- with safe_open(str(path), framework="pt") as f:
684
- meta = f.metadata() or {}
685
- if meta.get("machine"):
686
- m = meta["machine"]
687
- out.update(status="SKIP", machine=m,
688
- note=f"standalone machine — verify with {MACHINE_VERIFIER.get(m, 'machines.py')}")
689
- return out
690
- try:
691
- tensors = load_model(str(path))
692
- except Exception as e:
693
- out.update(error=f"load failed: {e}", status="ERROR")
694
- return out
695
-
696
- manifest = get_manifest(tensors)
697
- out.update(
698
- size_mb=path.stat().st_size / (1024 * 1024),
699
- tensors=len(tensors),
700
- params=sum(t.numel() for t in tensors.values()),
701
- manifest=manifest,
702
- )
703
-
704
- # Move to device
705
- tensors = {k: v.to(device) for k, v in tensors.items()}
706
-
707
- try:
708
- evaluator = BatchedFitnessEvaluator(device=device, model_path=str(path), tensors=tensors)
709
- population = create_population(tensors, pop_size=pop_size, device=device)
710
- t0 = time.perf_counter()
711
- fitness = evaluator.evaluate(population, debug=debug)
712
- elapsed = time.perf_counter() - t0
713
- f0 = float(fitness[0].item()) if pop_size == 1 else float(fitness.mean().item())
714
- out.update(
715
- fitness=f0,
716
- total_tests=evaluator.total_tests,
717
- elapsed_s=elapsed,
718
- categories={k: (float(v[0]), int(v[1])) for k, v in evaluator.category_scores.items()},
719
- status="PASS" if f0 >= 0.9999 else "FAIL",
720
- )
721
- except Exception as e:
722
- out.update(error=f"eval failed: {type(e).__name__}: {e}", status="ERROR")
723
- return out
724
-
725
- # Optional: CPU program test (8-bit CPU primitives are in every variant)
726
- if run_cpu_program:
727
- if manifest["memory_bytes"] >= PROGRAM_MIN_BYTES:
728
- try:
729
- cpu_tensors = {k: v.cpu() for k, v in tensors.items()}
730
- cpu = GenericThresholdCPU(cpu_tensors)
731
- mem, expected = builtin_program(manifest["addr_bits"])
732
- state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": mem, "halted": False}
733
- t0 = time.perf_counter()
734
- final, cycles = cpu.run(state, max_cycles=200)
735
- cpu_elapsed = time.perf_counter() - t0
736
- got = final["mem"][0x83]
737
- out["cpu_program"] = {
738
- "ok": got == expected,
739
- "got": got,
740
- "expected": expected,
741
- "cycles": cycles,
742
- "elapsed_s": cpu_elapsed,
743
- }
744
- if got != expected:
745
- out["status"] = "FAIL"
746
- except Exception as e:
747
- out["cpu_program"] = {"error": str(e)}
748
- else:
749
- out["cpu_program"] = {"skipped": f"mem={manifest['memory_bytes']}B < {PROGRAM_MIN_BYTES}"}
750
-
751
- # Wider-ALU chain test for 16/32-bit variants
752
- bits = manifest["data_bits"]
753
- if bits in (16, 32):
754
- try:
755
- alu_tensors = {k: v.cpu() for k, v in tensors.items()}
756
- alu = GenericThresholdALU(alu_tensors, bits)
757
- t0 = time.perf_counter()
758
- if bits == 16:
759
- x, y = 1234, 5678
760
- z, _ = alu.add_n(x, y, 16); assert z == (x + y) & 0xFFFF
761
- w, _ = alu.sub_n(z, x, 16); assert w == (z - x) & 0xFFFF, (w, z - x)
762
- gt = alu.cmp_n(z, x, "greaterthan", 16); assert gt == 1
763
- lt = alu.cmp_n(x, z, "lessthan", 16); assert lt == 1
764
- eq = alu.cmp_n(w, y, "eq", 16); assert eq == 1
765
- p = alu.mul_n(123, 5, 16); assert p == (123 * 5) & 0xFFFF
766
- else: # 32
767
- x, y = 1_000_000, 999_000
768
- z, _ = alu.sub_n(x, y, 32); assert z == 1_000
769
- s, _ = alu.add_n(z, x, 32); assert s == 1_001_000
770
- p = alu.mul_n(z, 100, 32); assert p == 100_000
771
- gt = alu.cmp_n(x, y, "greaterthan", 32); assert gt == 1
772
- lt = alu.cmp_n(y, x, "lessthan", 32); assert lt == 1
773
- eq = alu.cmp_n(p, 100_000, "equality", 32); assert eq == 1
774
- chain_dt = time.perf_counter() - t0
775
- out[f"alu_chain_{bits}"] = {"ok": True, "elapsed_s": chain_dt}
776
- except AssertionError as e:
777
- out[f"alu_chain_{bits}"] = {"ok": False, "error": f"chain mismatch: {e}"}
778
- out["status"] = "FAIL"
779
- except Exception as e:
780
- out[f"alu_chain_{bits}"] = {"ok": False, "error": f"{type(e).__name__}: {e}"}
781
- out["status"] = "FAIL"
782
-
783
- return out
784
-
785
-
786
- def print_row(r: Dict, show_cpu: bool) -> None:
787
- if r.get("status") == "SKIP":
788
- m = r.get("machine", "machine")
789
- print(f" {r['filename']:<48} SKIP ({m} — verify with {MACHINE_VERIFIER.get(m, 'machines.py')})")
790
- return
791
- if "error" in r:
792
- print(f" {r['filename']:<48} ERROR: {r['error'][:80]}")
793
- return
794
- m = r["manifest"]
795
- fit = f"{r['fitness']:.4f}" if r.get("fitness") is not None else "n/a"
796
- cpu_col = ""
797
- if show_cpu and "cpu_program" in r:
798
- cp = r["cpu_program"]
799
- if cp.get("ok"):
800
- cpu_col = f" CPU OK ({cp['cycles']}cyc/{cp['elapsed_s']:.1f}s)"
801
- elif "skipped" in cp:
802
- cpu_col = f" CPU SKIP"
803
- elif "error" in cp:
804
- cpu_col = f" CPU ERR"
805
- else:
806
- cpu_col = f" CPU FAIL ({cp.get('got')}!={cp.get('expected')})"
807
- chain_col = ""
808
- if show_cpu:
809
- for bits in (16, 32):
810
- key = f"alu_chain_{bits}"
811
- if key in r:
812
- ch = r[key]
813
- if ch.get("ok"):
814
- chain_col = f" ALU{bits} OK ({ch['elapsed_s']:.2f}s)"
815
- else:
816
- chain_col = f" ALU{bits} FAIL"
817
- print(
818
- f" {r['filename']:<48} d={m['data_bits']:>2}b a={m['addr_bits']:>2}b "
819
- f"mem={m['memory_bytes']:>6}B size={r['size_mb']:>6.1f}MB "
820
- f"params={r['params']:>10,} fit={fit:>6} tests={r['total_tests']:>5} "
821
- f"{r['status']:>5}{cpu_col}{chain_col}"
822
- )
823
-
824
-
825
- def main() -> int:
826
- parser = argparse.ArgumentParser(description="Variant-agnostic eval harness")
827
- parser.add_argument("path", help="Path to .safetensors file or directory of files")
828
- parser.add_argument("--device", default="cpu", help="cpu (default) or cuda")
829
- parser.add_argument("--pop_size", type=int, default=1)
830
- parser.add_argument("--debug", action="store_true", help="Per-circuit detail per file")
831
- parser.add_argument("--cpu-program", action="store_true",
832
- help="Also run a small assembled program through the threshold CPU "
833
- "(any variant with >= 132 B memory), plus a chained 16- or "
834
- "32-bit ALU sequence on wider variants")
835
- parser.add_argument("--json", action="store_true", help="Emit JSON results to stdout instead of a table")
836
- parser.add_argument("--cache-dir", default=".eval_cache",
837
- help="Directory for hash-keyed result cache "
838
- "(default: ./.eval_cache). Set to '' to disable.")
839
- parser.add_argument("--no-cache", action="store_true",
840
- help="Disable the result cache for this run.")
841
- args = parser.parse_args()
842
-
843
- files = list_safetensors(Path(args.path))
844
- if not files:
845
- print(f"No .safetensors files found under {args.path}", file=sys.stderr)
846
- return 2
847
-
848
- print(f"Evaluating {len(files)} file(s) on {args.device}\n")
849
- cache_enabled = bool(args.cache_dir) and not args.no_cache
850
- cache_dir = Path(args.cache_dir) if cache_enabled else None
851
- cache_opts = {
852
- "device": args.device,
853
- "pop_size": args.pop_size,
854
- "cpu_program": bool(args.cpu_program),
855
- }
856
- cache_hits = 0
857
- results = []
858
- fail_count = 0
859
- for f in files:
860
- print(f"=== {f.name}")
861
- cached = None
862
- key = None
863
- if cache_enabled:
864
- try:
865
- key = _cache_key(f, cache_opts)
866
- cached = _load_cache(cache_dir, key)
867
- except OSError:
868
- cached = None
869
- if cached is not None:
870
- r = cached
871
- cache_hits += 1
872
- print(f" (cache hit)")
873
- else:
874
- r = evaluate_one(f, device=args.device, pop_size=args.pop_size,
875
- debug=args.debug, run_cpu_program=args.cpu_program)
876
- # Don't cache ERROR results: a transient failure (OOM, interrupt)
877
- # would otherwise be replayed on every rerun of the same file.
878
- if cache_enabled and key is not None and r.get("status") != "ERROR":
879
- _save_cache(cache_dir, key, r)
880
- results.append(r)
881
- print_row(r, show_cpu=args.cpu_program)
882
- if r.get("status") not in ("PASS", "SKIP"):
883
- fail_count += 1
884
-
885
- if args.json:
886
- # Make it serialisable
887
- for r in results:
888
- r["manifest"] = {k: (int(v) if isinstance(v, float) and v.is_integer() else v)
889
- for k, v in r.get("manifest", {}).items()}
890
- print(json.dumps(results, indent=2, default=str))
891
- return fail_count
892
-
893
- # Summary
894
- print()
895
- print("=" * 100)
896
- print(" SUMMARY")
897
- print("=" * 100)
898
- for r in results:
899
- print_row(r, show_cpu=args.cpu_program)
900
-
901
- print()
902
- if fail_count == 0:
903
- print(f"ALL {len(files)} variants PASS")
904
- else:
905
- print(f"{fail_count}/{len(files)} variants FAIL")
906
- if cache_enabled:
907
- print(f"(cache: {cache_hits}/{len(files)} hits, dir={cache_dir})")
908
- return fail_count
909
-
910
-
911
- if __name__ == "__main__":
912
- sys.exit(main())