diff --git "a/eval.py" "b/eval.py" new file mode 100644--- /dev/null +++ "b/eval.py" @@ -0,0 +1,5260 @@ +""" +Unified Evaluation Suite for 8-bit Threshold Computer +====================================================== +GPU-batched evaluation with per-circuit reporting. +Includes CPU runtime for threshold-weight execution. + +Usage: + python eval.py # Run circuit evaluation + python eval.py --device cpu # CPU mode + python eval.py --pop_size 1000 # Population mode for evolution + python eval.py --cpu-test # Run CPU smoke test + +The single gate-routed CPU runtime is GenericThresholdCPU in eval_all.py +(manifest-sized); the pure-Python reference (CPUState / ref_step) lives here +for cross-checks. + +API (for prune_weights.py): + from eval import load_model, create_population, BatchedFitnessEvaluator +""" + +import argparse +import json +import os +import time +from collections import defaultdict +from dataclasses import dataclass, field +from typing import Callable, Dict, List, Optional, Tuple + +import torch +from safetensors import safe_open + + +MODEL_PATH = os.path.join(os.path.dirname(__file__), "neural_computer.safetensors") + + +@dataclass +class CircuitResult: + """Result for a single circuit test.""" + name: str + passed: int + total: int + failures: List[Tuple] = field(default_factory=list) + + @property + def success(self) -> bool: + return self.passed == self.total + + @property + def rate(self) -> float: + return self.passed / self.total if self.total > 0 else 0.0 + + +def heaviside(x: torch.Tensor) -> torch.Tensor: + """Threshold activation: 1 if x >= 0, else 0.""" + return (x >= 0).float() + + +def load_model(path: str = MODEL_PATH) -> Dict[str, torch.Tensor]: + """Load model tensors from safetensors.""" + with safe_open(path, framework='pt') as f: + return {name: f.get_tensor(name).float() for name in f.keys()} + + +def load_metadata(path: str = MODEL_PATH) -> Dict: + """Load metadata from safetensors (includes signal_registry).""" + with safe_open(path, framework='pt') as f: + meta = f.metadata() + if meta and 'signal_registry' in meta: + return {'signal_registry': json.loads(meta['signal_registry'])} + return {'signal_registry': {}} + + +def get_manifest(tensors: Dict[str, torch.Tensor]) -> Dict[str, int]: + """Extract manifest values from tensors. + + Returns dict with data_bits, addr_bits, memory_bytes, version. + Defaults to 8-bit data, 16-bit addr for legacy models. + """ + return { + 'data_bits': int(tensors.get('manifest.data_bits', torch.tensor([8.0])).item()), + 'addr_bits': int(tensors.get('manifest.addr_bits', + tensors.get('manifest.pc_width', torch.tensor([16.0]))).item()), + 'memory_bytes': int(tensors.get('manifest.memory_bytes', torch.tensor([65536.0])).item()), + 'version': float(tensors.get('manifest.version', torch.tensor([1.0])).item()), + } + + +def create_population( + base_tensors: Dict[str, torch.Tensor], + pop_size: int, + device: str = 'cuda' +) -> Dict[str, torch.Tensor]: + """Replicate base tensors for batched population evaluation.""" + return { + name: tensor.unsqueeze(0).expand(pop_size, *tensor.shape).clone().to(device) + for name, tensor in base_tensors.items() + } + + +# ============================================================================= +# CPU RUNTIME +# ============================================================================= + +FLAG_NAMES = ["Z", "N", "C", "V"] +CTRL_NAMES = ["HALT", "MEM_WE", "MEM_RE", "RESERVED"] + +PC_BITS = 16 +IR_BITS = 16 +REG_BITS = 8 +REG_COUNT = 4 +FLAG_BITS = 4 +SP_BITS = 16 +CTRL_BITS = 4 +MEM_BYTES = 65536 +MEM_BITS = MEM_BYTES * 8 + +STATE_BITS = PC_BITS + IR_BITS + (REG_BITS * REG_COUNT) + FLAG_BITS + SP_BITS + CTRL_BITS + MEM_BITS + + +def int_to_bits(value: int, width: int) -> List[int]: + return [(value >> (width - 1 - i)) & 1 for i in range(width)] + + +def bits_to_int(bits: List[int]) -> int: + value = 0 + for bit in bits: + value = (value << 1) | int(bit) + return value + + +def bits_msb_to_lsb(bits: List[int]) -> List[int]: + return list(reversed(bits)) + + +@dataclass +class CPUState: + pc: int + ir: int + regs: List[int] + flags: List[int] + sp: int + ctrl: List[int] + mem: List[int] + + def copy(self) -> 'CPUState': + return CPUState( + pc=int(self.pc), + ir=int(self.ir), + regs=[int(r) for r in self.regs], + flags=[int(f) for f in self.flags], + sp=int(self.sp), + ctrl=[int(c) for c in self.ctrl], + mem=[int(m) for m in self.mem], + ) + + +def pack_state(state: CPUState) -> List[int]: + bits: List[int] = [] + bits.extend(int_to_bits(state.pc, PC_BITS)) + bits.extend(int_to_bits(state.ir, IR_BITS)) + for reg in state.regs: + bits.extend(int_to_bits(reg, REG_BITS)) + bits.extend([int(f) for f in state.flags]) + bits.extend(int_to_bits(state.sp, SP_BITS)) + bits.extend([int(c) for c in state.ctrl]) + for byte in state.mem: + bits.extend(int_to_bits(byte, REG_BITS)) + return bits + + +def unpack_state(bits: List[int]) -> CPUState: + if len(bits) != STATE_BITS: + raise ValueError(f"Expected {STATE_BITS} bits, got {len(bits)}") + + idx = 0 + pc = bits_to_int(bits[idx:idx + PC_BITS]) + idx += PC_BITS + ir = bits_to_int(bits[idx:idx + IR_BITS]) + idx += IR_BITS + + regs = [] + for _ in range(REG_COUNT): + regs.append(bits_to_int(bits[idx:idx + REG_BITS])) + idx += REG_BITS + + flags = [int(b) for b in bits[idx:idx + FLAG_BITS]] + idx += FLAG_BITS + + sp = bits_to_int(bits[idx:idx + SP_BITS]) + idx += SP_BITS + + ctrl = [int(b) for b in bits[idx:idx + CTRL_BITS]] + idx += CTRL_BITS + + mem = [] + for _ in range(MEM_BYTES): + mem.append(bits_to_int(bits[idx:idx + REG_BITS])) + idx += REG_BITS + + return CPUState(pc=pc, ir=ir, regs=regs, flags=flags, sp=sp, ctrl=ctrl, mem=mem) + + +def decode_ir(ir: int) -> Tuple[int, int, int, int]: + opcode = (ir >> 12) & 0xF + rd = (ir >> 10) & 0x3 + rs = (ir >> 8) & 0x3 + imm8 = ir & 0xFF + return opcode, rd, rs, imm8 + + +def flags_from_result(result: int, carry: int, overflow: int) -> Tuple[int, int, int, int]: + z = 1 if result == 0 else 0 + n = 1 if (result & 0x80) else 0 + c = 1 if carry else 0 + v = 1 if overflow else 0 + return z, n, c, v + + +def alu_add(a: int, b: int) -> Tuple[int, int, int]: + full = a + b + result = full & 0xFF + carry = 1 if full > 0xFF else 0 + overflow = 1 if (((a ^ result) & (b ^ result)) & 0x80) else 0 + return result, carry, overflow + + +def alu_sub(a: int, b: int) -> Tuple[int, int, int]: + full = (a - b) & 0x1FF + result = full & 0xFF + carry = 1 if a >= b else 0 + overflow = 1 if (((a ^ b) & (a ^ result)) & 0x80) else 0 + return result, carry, overflow + + +def ref_step(state: CPUState) -> CPUState: + """Reference CPU cycle (pure Python arithmetic).""" + if state.ctrl[0] == 1: + return state.copy() + + s = state.copy() + + hi = s.mem[s.pc] + lo = s.mem[(s.pc + 1) & 0xFFFF] + s.ir = ((hi & 0xFF) << 8) | (lo & 0xFF) + next_pc = (s.pc + 2) & 0xFFFF + + opcode, rd, rs, imm8 = decode_ir(s.ir) + a = s.regs[rd] + b = s.regs[rs] + + addr16 = None + next_pc_ext = next_pc + if opcode in (0xA, 0xB, 0xC, 0xD, 0xE): + addr_hi = s.mem[next_pc] + addr_lo = s.mem[(next_pc + 1) & 0xFFFF] + addr16 = ((addr_hi & 0xFF) << 8) | (addr_lo & 0xFF) + next_pc_ext = (next_pc + 2) & 0xFFFF + + write_result = True + result = a + carry = 0 + overflow = 0 + + if opcode == 0x0: + result, carry, overflow = alu_add(a, b) + elif opcode == 0x1: + result, carry, overflow = alu_sub(a, b) + elif opcode == 0x2: + result = a & b + elif opcode == 0x3: + result = a | b + elif opcode == 0x4: + result = a ^ b + elif opcode == 0x5: + result = (a << 1) & 0xFF + elif opcode == 0x6: + result = (a >> 1) & 0xFF + elif opcode == 0x7: + result = (a * b) & 0xFF + elif opcode == 0x8: + if b == 0: + result = 0xFF + else: + result = a // b + elif opcode == 0x9: + result, carry, overflow = alu_sub(a, b) + write_result = False + elif opcode == 0xA: + result = s.mem[addr16] + elif opcode == 0xB: + s.mem[addr16] = b & 0xFF + write_result = False + elif opcode == 0xC: + s.pc = addr16 & 0xFFFF + write_result = False + elif opcode == 0xD: + cond_type = imm8 & 0x7 + if cond_type == 0: + take_branch = s.flags[0] == 1 + elif cond_type == 1: + take_branch = s.flags[0] == 0 + elif cond_type == 2: + take_branch = s.flags[2] == 1 + elif cond_type == 3: + take_branch = s.flags[2] == 0 + elif cond_type == 4: + take_branch = s.flags[1] == 1 + elif cond_type == 5: + take_branch = s.flags[1] == 0 + elif cond_type == 6: + take_branch = s.flags[3] == 1 + else: + take_branch = s.flags[3] == 0 + if take_branch: + s.pc = addr16 & 0xFFFF + else: + s.pc = next_pc_ext + write_result = False + elif opcode == 0xE: + ret_addr = next_pc_ext & 0xFFFF + s.sp = (s.sp - 1) & 0xFFFF + s.mem[s.sp] = (ret_addr >> 8) & 0xFF + s.sp = (s.sp - 1) & 0xFFFF + s.mem[s.sp] = ret_addr & 0xFF + s.pc = addr16 & 0xFFFF + write_result = False + elif opcode == 0xF: + s.ctrl[0] = 1 + write_result = False + + # Flag policy: only ADD, SUB, MUL, and CMP write Z/N/C/V (MUL clears C + # and V). Bitwise, shift, DIV, LOAD, and STORE leave FLAGS unchanged. + if opcode in (0x0, 0x1, 0x7, 0x9): + s.flags = list(flags_from_result(result, carry, overflow)) + + if write_result: + s.regs[rd] = result & 0xFF + + if opcode not in (0xC, 0xD, 0xE): + s.pc = next_pc_ext + + return s + + +def ref_run_until_halt(state: CPUState, max_cycles: int = 256) -> Tuple[CPUState, int]: + """Reference execution loop.""" + s = state.copy() + for i in range(max_cycles): + if s.ctrl[0] == 1: + return s, i + s = ref_step(s) + return s, max_cycles + + +def encode_instr(opcode: int, rd: int, rs: int, imm8: int) -> int: + return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm8 & 0xFF) + + +def write_instr(mem: List[int], addr: int, instr: int) -> None: + mem[addr & 0xFFFF] = (instr >> 8) & 0xFF + mem[(addr + 1) & 0xFFFF] = instr & 0xFF + + +def write_addr(mem: List[int], addr: int, value: int) -> None: + mem[addr & 0xFFFF] = (value >> 8) & 0xFF + mem[(addr + 1) & 0xFFFF] = value & 0xFF + + +def _fill_smoke_program(mem: List[int]) -> None: + """LOAD/ADD/STORE, MUL with a high-bit operand, and a SUB/JNZ loop.""" + write_instr(mem, 0x0000, encode_instr(0xA, 0, 0, 0x00)) + write_addr(mem, 0x0002, 0x0100) + write_instr(mem, 0x0004, encode_instr(0xA, 1, 0, 0x00)) + write_addr(mem, 0x0006, 0x0101) + write_instr(mem, 0x0008, encode_instr(0x0, 0, 1, 0x00)) + write_instr(mem, 0x000A, encode_instr(0xB, 0, 0, 0x00)) + write_addr(mem, 0x000C, 0x0102) + write_instr(mem, 0x000E, encode_instr(0xA, 2, 0, 0x00)) + write_addr(mem, 0x0010, 0x0103) + write_instr(mem, 0x0012, encode_instr(0xA, 3, 0, 0x00)) + write_addr(mem, 0x0014, 0x0104) + write_instr(mem, 0x0016, encode_instr(0x7, 2, 3, 0x00)) + write_instr(mem, 0x0018, encode_instr(0xB, 0, 2, 0x00)) + write_addr(mem, 0x001A, 0x0105) + write_instr(mem, 0x001C, encode_instr(0xA, 0, 0, 0x00)) + write_addr(mem, 0x001E, 0x0106) + write_instr(mem, 0x0020, encode_instr(0xA, 1, 0, 0x00)) + write_addr(mem, 0x0022, 0x0107) + write_instr(mem, 0x0024, encode_instr(0xA, 3, 0, 0x00)) + write_addr(mem, 0x0026, 0x0108) + write_instr(mem, 0x0028, encode_instr(0x0, 1, 0, 0x00)) # loop: R1 += R0 + write_instr(mem, 0x002A, encode_instr(0x1, 0, 3, 0x00)) # R0 -= 1 + write_instr(mem, 0x002C, encode_instr(0xD, 0, 0, 0x01)) # JNZ loop + write_addr(mem, 0x002E, 0x0028) + write_instr(mem, 0x0030, encode_instr(0xB, 0, 1, 0x00)) + write_addr(mem, 0x0032, 0x0109) + write_instr(mem, 0x0034, encode_instr(0xF, 0, 0, 0x00)) + mem[0x0100] = 5 + mem[0x0101] = 7 + mem[0x0103] = 2 + mem[0x0104] = 131 + mem[0x0106] = 3 + mem[0x0107] = 0 + mem[0x0108] = 1 + + +def run_smoke_test() -> int: + """Smoke test through the single gate-routed CPU runtime + (GenericThresholdCPU), cross-checked against the pure-Python reference: + + 1. LOAD 5, LOAD 7, ADD, STORE -> MEM[0x0102] = 12 + 2. LOAD 2, LOAD 131, MUL, STORE -> MEM[0x0105] = (2*131) & 0xFF = 6 + 3. Countdown loop 3+2+1 via SUB/JNZ -> MEM[0x0109] = 6 + + Runs on the 1 KB variant so the threshold pass finishes in seconds; the + reference runs at 64 KB in pure Python. + """ + import os + from eval_all import GenericThresholdCPU # lazy: avoids eval<->eval_all cycle + + expected = {0x0102: 12, 0x0105: 6, 0x0109: 6} + + print("Running reference implementation...") + ref_mem = [0] * 65536 + _fill_smoke_program(ref_mem) + state = CPUState(pc=0, ir=0, regs=[0, 0, 0, 0], flags=[0, 0, 0, 0], + sp=0xFFFE, ctrl=[0, 0, 0, 0], mem=ref_mem) + final, cycles = ref_run_until_halt(state, max_cycles=40) + assert final.ctrl[0] == 1, "HALT flag not set" + for addr, want in expected.items(): + assert final.mem[addr] == want, f"MEM[{addr:#06x}] expected {want}, got {final.mem[addr]}" + print(f" Reference: ADD={final.mem[0x0102]}, MUL={final.mem[0x0105]}, " + f"LOOP={final.mem[0x0109]}, cycles={cycles}") + + print("Running threshold-weight implementation (GenericThresholdCPU, 1 KB)...") + path = os.path.join(os.path.dirname(__file__), "variants", + "neural_computer8_small.safetensors") + tensors = {} + with safe_open(path, framework="pt") as f: + for name in f.keys(): + tensors[name] = f.get_tensor(name).float() + cpu = GenericThresholdCPU(tensors) + t_mem = [0] * 1024 + _fill_smoke_program(t_mem) + t_state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": t_mem, "halted": False} + t_final, t_cycles = cpu.run(t_state, max_cycles=40) + assert t_final["halted"], "Threshold HALT not reached" + for addr, want in expected.items(): + assert t_final["mem"][addr] == want, ( + f"Threshold MEM[{addr:#06x}] mismatch: {t_final['mem'][addr]} != {want}") + assert t_cycles == cycles, f"cycle count mismatch: {t_cycles} != {cycles}" + print(f" Threshold: ADD={t_final['mem'][0x0102]}, MUL={t_final['mem'][0x0105]}, " + f"LOOP={t_final['mem'][0x0109]}, cycles={t_cycles}") + + print("\nSmoke test: PASSED") + return 0 + + +# ============================================================================= +# NETLIST EVALUATION (metadata-driven) +# ============================================================================= + +class NetlistEvaluator: + """Evaluate a self-contained circuit from its shipped wiring metadata. + + The gate graph is built from the .inputs tensors and the header signal + registry — the same artifacts safetensors2verilog consumes — so a test + driven through this class proves the file itself encodes the circuit, + with no wiring knowledge living in Python. + + External signals are names starting with '$' (plus the constants #0/#1); + the caller binds them per evaluation. Gate outputs are published under + their full gate names. Evaluation is batched over test vectors and, + for population dicts, over population slots. + """ + + def __init__(self, tensors: Dict[str, torch.Tensor], + signal_registry: Dict[str, str], prefix: str, + pop_size: int = 1, levels: bool = True): + id_to_name = {int(k): v for k, v in signal_registry.items()} + self.prefix = prefix + self.pop_size = pop_size + self.gates: Dict[str, Tuple[torch.Tensor, torch.Tensor, List[str]]] = {} + for key, t in tensors.items(): + if not key.endswith('.inputs'): + continue + gate = key[: -len('.inputs')] + if gate != prefix and not gate.startswith(prefix + '.'): + continue + w = tensors.get(gate + '.weight') + b = tensors.get(gate + '.bias') + if w is None or b is None: + continue + inp = t.reshape(pop_size, -1)[0] if pop_size > 1 else t.flatten() + fan = inp.numel() + if w.numel() != fan * pop_size: + continue # packed multi-gate tensor; not a single netlist gate + names = [id_to_name[int(i)] for i in inp.tolist()] + self.gates[gate] = (w.float().view(pop_size, fan), + b.float().view(pop_size), names) + if not self.gates: + raise KeyError(f"no wired gates under prefix {prefix}") + + # Topological order (Kahn). Inputs that are gates in this circuit are + # dependencies; everything else must be bound externally. + indeg = {g: 0 for g in self.gates} + consumers: Dict[str, List[str]] = {} + for g, (_, _, names) in self.gates.items(): + for n in names: + if n in self.gates: + indeg[g] += 1 + consumers.setdefault(n, []).append(g) + order = [g for g, d in indeg.items() if d == 0] + i = 0 + while i < len(order): + for c in consumers.get(order[i], []): + indeg[c] -= 1 + if indeg[c] == 0: + order.append(c) + i += 1 + if len(order) != len(self.gates): + cyc = sorted(set(self.gates) - set(order))[:5] + raise ValueError(f"wiring cycle under {prefix}: {cyc}") + self.order = order + + # Leveled plan: one padded tensor op per topological level instead of + # one Python step per gate. Turns thousands of tiny ops into ~depth + # batched ops (a MULH-class netlist drops from seconds to tens of ms). + self._plan = None + if levels: + self._build_levels(id_to_name) + + def _build_levels(self, id_to_name): + # Signal slots: constants, all externals, all gate outputs. + ext = self.external_names() + names = ['#0', '#1'] + ext + self.order + self.slot = {n: i for i, n in enumerate(names)} + self.n_sig = len(names) + self.ext_names = ext + + depth = {} + for n in names: + if n not in self.gates: + depth[n] = 0 + for g in self.order: + _, _, ins = self.gates[g] + depth[g] = 1 + max((depth[n] for n in ins), default=0) + by_level: Dict[int, List[str]] = {} + for g in self.order: + by_level.setdefault(depth[g], []).append(g) + + plan = [] + for lvl in sorted(by_level): + gs = by_level[lvl] + max_fan = max(len(self.gates[g][2]) for g in gs) + n_g = len(gs) + idx = torch.zeros(n_g, max_fan, dtype=torch.long) + w = torch.zeros(n_g, max_fan, self.pop_size) + b = torch.zeros(n_g, self.pop_size) + out = torch.zeros(n_g, dtype=torch.long) + for r, g in enumerate(gs): + wt, bs, ins = self.gates[g] + out[r] = self.slot[g] + b[r] = bs + for c, n in enumerate(ins): + idx[r, c] = self.slot[n] + w[r, c] = wt[:, c] + plan.append((idx, w, b, out)) + self._plan = plan + + def external_names(self) -> List[str]: + """Every non-gate, non-constant signal the circuit consumes.""" + out = set() + for _, (_, _, names) in self.gates.items(): + for n in names: + if n not in self.gates and n not in ('#0', '#1'): + out.add(n) + return sorted(out) + + def run(self, external: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """Evaluate the circuit. Each external is a [num_tests] tensor (or a + scalar); every returned gate output is [num_tests, pop_size].""" + first = next(iter(external.values())) + if not torch.is_tensor(first): + num_tests = 1 + else: + num_tests = first.shape[0] if first.dim() else 1 + device = self.gates[self.order[0]][0].device + + def expand(v): + t = v if torch.is_tensor(v) else torch.tensor(float(v)) + t = t.float().to(device).reshape(-1) + return t.unsqueeze(1).expand(num_tests, self.pop_size) + + if self._plan is not None: + V = torch.zeros(self.n_sig, num_tests, self.pop_size, device=device) + V[self.slot['#1']] = 1.0 + for k, v in external.items(): + V[self.slot[k]] = expand(v) + for idx, w, b, out in self._plan: + # gathered: [n_g, max_fan, num_tests, pop] + gathered = V[idx] + acc = (gathered * w[:, :, None, :]).sum(1) + b[:, None, :] + V[out] = (acc >= 0).float() + return _SlotView(V, self.slot) + + values: Dict[str, torch.Tensor] = { + '#0': torch.zeros(num_tests, self.pop_size, device=device), + '#1': torch.ones(num_tests, self.pop_size, device=device), + } + for k, v in external.items(): + values[k] = expand(v) + for g in self.order: + w, b, names = self.gates[g] + acc = b.unsqueeze(0).expand(num_tests, self.pop_size).clone() + for k, n in enumerate(names): + acc = acc + w[:, k] * values[n] + values[g] = (acc >= 0).float() + return values + + +class _SlotView: + """Read gate outputs by name from a packed [n_sig, num_tests, pop] tensor.""" + __slots__ = ("_V", "_slot") + + def __init__(self, V, slot): + self._V = V + self._slot = slot + + def __getitem__(self, name): + return self._V[self._slot[name]] + + def __contains__(self, name): + return name in self._slot + + +def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float: + """Decode an IEEE 754 word to a Python float. float64 represents every + float16/float32 value exactly, so the oracle comparisons are exact.""" + sign = (word >> (exp_bits + frac_bits)) & 1 + exp = (word >> frac_bits) & ((1 << exp_bits) - 1) + frac = word & ((1 << frac_bits) - 1) + bias = (1 << (exp_bits - 1)) - 1 + if exp == (1 << exp_bits) - 1: + if frac: + return float('nan') + return float('-inf') if sign else float('inf') + if exp == 0: + v = frac * 2.0 ** (1 - bias - frac_bits) + else: + v = (frac + (1 << frac_bits)) * 2.0 ** (exp - bias - frac_bits) + return -v if sign else v + + +def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int]]: + """Directed IEEE edge encodings (every category, both signs) plus seeded + random words for a family.""" + import random + E, F = exp_bits, frac_bits + emax = (1 << E) - 1 + fmax = (1 << F) - 1 + bias = (1 << (E - 1)) - 1 + + def word(s, e, f): + return (s << (E + F)) | (e << F) | f + + directed = [] + for s in (0, 1): + directed += [ + word(s, 0, 0), # +-0 + word(s, 0, 1), # smallest subnormal + word(s, 0, fmax), # largest subnormal + word(s, 1, 0), # smallest normal + word(s, bias, 0), # +-1.0 + word(s, bias, 1 << (F - 1)), # +-1.5 + word(s, emax - 1, fmax), # largest normal + word(s, emax, 0), # +-inf + word(s, emax, 1), # NaN, minimal payload + word(s, emax, fmax), # NaN, full payload + ] + rng = random.Random(0xF10A7 + E) + randoms = [rng.getrandbits(1 + E + F) for _ in range(24)] + return directed, randoms + + +def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int: + """Expected product word under the documented contract: exact IEEE + specials (NaN, infinities, signed zeros), flush-to-zero subnormals, + round-to-nearest-even mantissa. Pure integer arithmetic, so exact.""" + E, F = exp_bits, frac_bits + emax = (1 << E) - 1 + fmask = (1 << F) - 1 + bias = (1 << (E - 1)) - 1 + qnan = (emax << F) | (1 << (F - 1)) + sa, ea, fa = (aw >> (E + F)) & 1, (aw >> F) & emax, aw & fmask + sb, eb, fb = (bw >> (E + F)) & 1, (bw >> F) & emax, bw & fmask + s = sa ^ sb + a_nan = ea == emax and fa != 0 + b_nan = eb == emax and fb != 0 + a_inf = ea == emax and fa == 0 + b_inf = eb == emax and fb == 0 + a_zero = ea == 0 # flush-to-zero + b_zero = eb == 0 + inf_w = (s << (E + F)) | (emax << F) + zero_w = s << (E + F) + if a_nan or b_nan or (a_inf and b_zero) or (b_inf and a_zero): + return qnan + if a_inf or b_inf: + return inf_w + if a_zero or b_zero: + return zero_w + Ma, Mb = (1 << F) | fa, (1 << F) | fb + P = Ma * Mb + exp_r = ea + eb - bias + shift = F + 1 if P >= (1 << (2 * F + 1)) else F + if shift == F + 1: + exp_r += 1 + frac = (P >> shift) & fmask + guard = (P >> (shift - 1)) & 1 + below = P & ((1 << (shift - 1)) - 1) + if guard and ((frac & 1) or below): # round-to-nearest-even + frac += 1 + if frac > fmask: + frac = 0 + exp_r += 1 + if exp_r >= emax: + return inf_w + if exp_r <= 0: + return zero_w + return (s << (E + F)) | (exp_r << F) | frac + + +def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int: + """Expected quotient word under the documented contract: exact IEEE + specials (NaN, infinities, x/0 -> inf, 0/0 and inf/inf -> NaN, signed + zeros), flush-to-zero subnormals, round-to-nearest-even mantissa.""" + E, F = exp_bits, frac_bits + emax = (1 << E) - 1 + fmask = (1 << F) - 1 + bias = (1 << (E - 1)) - 1 + qnan = (emax << F) | (1 << (F - 1)) + sa, ea, fa = (aw >> (E + F)) & 1, (aw >> F) & emax, aw & fmask + sb, eb, fb = (bw >> (E + F)) & 1, (bw >> F) & emax, bw & fmask + s = sa ^ sb + a_nan = ea == emax and fa != 0 + b_nan = eb == emax and fb != 0 + a_inf = ea == emax and fa == 0 + b_inf = eb == emax and fb == 0 + a_zero = ea == 0 # flush-to-zero + b_zero = eb == 0 + inf_w = (s << (E + F)) | (emax << F) + zero_w = s << (E + F) + if a_nan or b_nan or (a_zero and b_zero) or (a_inf and b_inf): + return qnan + if a_inf or b_zero: + return inf_w + if a_zero or b_inf: + return zero_w + Ma, Mb = (1 << F) | fa, (1 << F) | fb + q0 = 1 if Ma >= Mb else 0 + exp_r = ea - eb + bias - 1 + q0 + num = Ma << (F + 2 - q0) # implicit 1 + F frac + guard bits + Q = num // Mb + rem = num % Mb + frac = (Q >> 1) & fmask + guard = Q & 1 + sticky = 1 if rem else 0 + if guard and ((frac & 1) or sticky): # round-to-nearest-even + frac += 1 + if frac > fmask: + frac = 0 + exp_r += 1 + if exp_r >= emax: + return inf_w + if exp_r <= 0: + return zero_w + return (s << (E + F)) | (exp_r << F) | frac + + +def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int: + """Expected sum word under the documented contract: exact IEEE specials + (NaN, infinities, opposite-sign infinities -> NaN, signed zeros, exact + cancellation -> +0), flush-to-zero subnormals (a zero operand passes the + other through verbatim), round-to-nearest-even mantissa.""" + E, F = exp_bits, frac_bits + emax = (1 << E) - 1 + fmask = (1 << F) - 1 + qnan = (emax << F) | (1 << (F - 1)) + sa, ea, fa = (aw >> (E + F)) & 1, (aw >> F) & emax, aw & fmask + sb, eb, fb = (bw >> (E + F)) & 1, (bw >> F) & emax, bw & fmask + a_nan = ea == emax and fa != 0 + b_nan = eb == emax and fb != 0 + a_inf = ea == emax and fa == 0 + b_inf = eb == emax and fb == 0 + a_zero = ea == 0 # flush-to-zero + b_zero = eb == 0 + if a_nan or b_nan or (a_inf and b_inf and sa != sb): + return qnan + if a_inf or b_inf: + s = sa if a_inf else sb + return (s << (E + F)) | (emax << F) + if a_zero and b_zero: + return (sa & sb) << (E + F) + if a_zero: + return bw + if b_zero: + return aw + Ma, Mb = (1 << F) | fa, (1 << F) | fb + pla, plb = (ea << F) | fa, (eb << F) | fb + if pla >= plb: + sL, ML, eL, MS, eS = sa, Ma, ea, Mb, eb + else: + sL, ML, eL, MS, eS = sb, Mb, eb, Ma, ea + d = eL - eS + # Exact fixed-point value with G extra low bits below the mantissa LSB, + # so guard/round/sticky are all recoverable for round-to-nearest-even. + G = F + 4 + total = (ML << (d + G)) + (MS << G) if sa == sb else (ML << (d + G)) - (MS << G) + if total == 0: + return 0 # exact cancellation -> +0 + t = total.bit_length() - 1 + # Normalize so the leading 1 sits at bit position (F + G); the mantissa + # is the top F+1 bits, then guard/round/sticky below. + exp_r = eS + (t - G) - F + lead = F + G + if t >= lead: + sh = t - lead + mant_ext = total >> sh + sticky_low = 1 if (total & ((1 << sh) - 1)) else 0 + else: + mant_ext = total << (lead - t) + sticky_low = 0 + frac = (mant_ext >> G) & fmask + guard = (mant_ext >> (G - 1)) & 1 + below = (mant_ext & ((1 << (G - 1)) - 1)) or sticky_low + if guard and ((frac & 1) or below): # round-to-nearest-even + frac += 1 + if frac > fmask: + frac = 0 + exp_r += 1 + inf_w = (sL << (E + F)) | (emax << F) + zero_w = sL << (E + F) + if exp_r >= emax: + return inf_w + if exp_r <= 0: + return zero_w + return (sL << (E + F)) | (exp_r << F) | frac + + +# ============================================================================= +# CIRCUIT EVALUATION +# ============================================================================= + +class BatchedFitnessEvaluator: + """ + GPU-batched fitness evaluator with per-circuit reporting. + Tests all circuits comprehensively. + """ + + def __init__(self, device: str = 'cuda', model_path: str = MODEL_PATH, tensors: Dict[str, torch.Tensor] = None): + self.device = device + self.model_path = model_path + self.metadata = load_metadata(model_path) + self.signal_registry = self.metadata.get('signal_registry', {}) + self.results: List[CircuitResult] = [] + self.category_scores: Dict[str, Tuple[float, int]] = {} + self.total_tests = 0 + + # Get manifest for N-bit support + if tensors is not None: + self.manifest = get_manifest(tensors) + else: + base_tensors = load_model(model_path) + self.manifest = get_manifest(base_tensors) + self.data_bits = self.manifest['data_bits'] + self.addr_bits = self.manifest['addr_bits'] + + self._setup_tests() + + def _setup_tests(self): + """Pre-compute test vectors on device.""" + d = self.device + + # 2-input truth table [4, 2] + self.tt2 = torch.tensor( + [[0, 0], [0, 1], [1, 0], [1, 1]], + device=d, dtype=torch.float32 + ) + + # 3-input truth table [8, 3] + self.tt3 = torch.tensor([ + [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], + [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1] + ], device=d, dtype=torch.float32) + + # Boolean gate expected outputs + self.expected = { + 'and': torch.tensor([0, 0, 0, 1], device=d, dtype=torch.float32), + 'or': torch.tensor([0, 1, 1, 1], device=d, dtype=torch.float32), + 'nand': torch.tensor([1, 1, 1, 0], device=d, dtype=torch.float32), + 'nor': torch.tensor([1, 0, 0, 0], device=d, dtype=torch.float32), + 'xor': torch.tensor([0, 1, 1, 0], device=d, dtype=torch.float32), + 'xnor': torch.tensor([1, 0, 0, 1], device=d, dtype=torch.float32), + 'implies': torch.tensor([1, 1, 0, 1], device=d, dtype=torch.float32), + 'biimplies': torch.tensor([1, 0, 0, 1], device=d, dtype=torch.float32), + 'not': torch.tensor([1, 0], device=d, dtype=torch.float32), + 'ha_sum': torch.tensor([0, 1, 1, 0], device=d, dtype=torch.float32), + 'ha_carry': torch.tensor([0, 0, 0, 1], device=d, dtype=torch.float32), + 'fa_sum': torch.tensor([0, 1, 1, 0, 1, 0, 0, 1], device=d, dtype=torch.float32), + 'fa_cout': torch.tensor([0, 0, 0, 1, 0, 1, 1, 1], device=d, dtype=torch.float32), + } + + # NOT gate inputs + self.not_inputs = torch.tensor([[0], [1]], device=d, dtype=torch.float32) + + # 8-bit test values + self.test_8bit = torch.tensor([ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255, + 0b10101010, 0b01010101, 0b11110000, 0b00001111, + 0b11001100, 0b00110011, 0b10000001, 0b01111110 + ], device=d, dtype=torch.long) + + # Bit representations [num_vals, 8] + self.test_8bit_bits = torch.stack([ + ((self.test_8bit >> (7 - i)) & 1).float() for i in range(8) + ], dim=1) + + # Comparator test pairs + comp_tests = [ + (0, 0), (1, 0), (0, 1), (5, 3), (3, 5), (5, 5), + (255, 0), (0, 255), (128, 127), (127, 128), + (100, 99), (99, 100), (64, 32), (32, 64), + (1, 1), (254, 255), (255, 254), (128, 128), + (0, 128), (128, 0), (64, 64), (192, 192), + (15, 16), (16, 15), (240, 239), (239, 240), + (85, 170), (170, 85), (0xAA, 0x55), (0x55, 0xAA), + (0x0F, 0xF0), (0xF0, 0x0F), (0x33, 0xCC), (0xCC, 0x33), + (2, 3), (3, 2), (126, 127), (127, 126), + (129, 128), (128, 129), (200, 199), (199, 200), + (50, 51), (51, 50), (10, 20), (20, 10), + (100, 100), (200, 200), (77, 77), (0, 0) + ] + self.comp_a = torch.tensor([c[0] for c in comp_tests], device=d, dtype=torch.long) + self.comp_b = torch.tensor([c[1] for c in comp_tests], device=d, dtype=torch.long) + + # Modular test range + self.mod_test = torch.arange(256, device=d, dtype=torch.long) + + # 32-bit test values (strategic sampling) + self.test_32bit = torch.tensor([ + 0, 1, 2, 255, 256, 65535, 65536, + 0x7FFFFFFF, 0x80000000, 0xFFFFFFFF, + 0x12345678, 0xDEADBEEF, 0xCAFEBABE, + 1000000, 1000000000, 2147483647, + 0x55555555, 0xAAAAAAAA, 0x0F0F0F0F, 0xF0F0F0F0 + ], device=d, dtype=torch.long) + + # 32-bit comparator test pairs + comp32_tests = [ + (0, 0), (1, 0), (0, 1), (1000, 999), (999, 1000), + (0xFFFFFFFF, 0), (0, 0xFFFFFFFF), + (0x80000000, 0x7FFFFFFF), (0x7FFFFFFF, 0x80000000), + (1000000, 1000000), (0x12345678, 0x12345678), + (0xDEADBEEF, 0xCAFEBABE), (0xCAFEBABE, 0xDEADBEEF), + (256, 255), (255, 256), (65536, 65535), (65535, 65536), + ] + self.comp32_a = torch.tensor([c[0] for c in comp32_tests], device=d, dtype=torch.long) + self.comp32_b = torch.tensor([c[1] for c in comp32_tests], device=d, dtype=torch.long) + + def _record(self, name: str, passed: int, total: int, failures: List[Tuple] = None): + """Record a circuit test result.""" + self.results.append(CircuitResult( + name=name, + passed=passed, + total=total, + failures=failures or [] + )) + + # ========================================================================= + # BOOLEAN GATES + # ========================================================================= + + def _test_single_gate(self, pop: Dict, prefix: str, inputs: torch.Tensor, + expected: torch.Tensor) -> torch.Tensor: + """Test single-layer gate (AND, OR, NAND, NOR, IMPLIES).""" + pop_size = next(iter(pop.values())).shape[0] + w = pop[f'{prefix}.weight'] + b = pop[f'{prefix}.bias'] + + # [num_tests, pop_size] + out = heaviside(inputs @ w.view(pop_size, -1).T + b.view(pop_size)) + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i, (inp, exp, got) in enumerate(zip(inputs, expected, out[:, 0])): + if exp.item() != got.item(): + failures.append((inp.tolist(), exp.item(), got.item())) + + self._record(prefix, int(correct[0].item()), len(expected), failures) + return correct + + def _test_twolayer_gate(self, pop: Dict, prefix: str, inputs: torch.Tensor, + expected: torch.Tensor) -> torch.Tensor: + """Test two-layer gate (XOR, XNOR, BIIMPLIES).""" + pop_size = next(iter(pop.values())).shape[0] + + # Layer 1 + w1_n1 = pop[f'{prefix}.layer1.neuron1.weight'] + b1_n1 = pop[f'{prefix}.layer1.neuron1.bias'] + w1_n2 = pop[f'{prefix}.layer1.neuron2.weight'] + b1_n2 = pop[f'{prefix}.layer1.neuron2.bias'] + + h1 = heaviside(inputs @ w1_n1.view(pop_size, -1).T + b1_n1.view(pop_size)) + h2 = heaviside(inputs @ w1_n2.view(pop_size, -1).T + b1_n2.view(pop_size)) + hidden = torch.stack([h1, h2], dim=-1) + + # Layer 2 + w2 = pop[f'{prefix}.layer2.weight'] + b2 = pop[f'{prefix}.layer2.bias'] + out = heaviside((hidden * w2.view(pop_size, 2)).sum(-1) + b2.view(pop_size)) + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i, (inp, exp, got) in enumerate(zip(inputs, expected, out[:, 0])): + if exp.item() != got.item(): + failures.append((inp.tolist(), exp.item(), got.item())) + + self._record(prefix, int(correct[0].item()), len(expected), failures) + return correct + + def _test_xor_ornand(self, pop: Dict, prefix: str, inputs: torch.Tensor, + expected: torch.Tensor) -> torch.Tensor: + """Test XOR with or/nand layer naming.""" + pop_size = next(iter(pop.values())).shape[0] + + w_or = pop[f'{prefix}.layer1.or.weight'] + b_or = pop[f'{prefix}.layer1.or.bias'] + w_nand = pop[f'{prefix}.layer1.nand.weight'] + b_nand = pop[f'{prefix}.layer1.nand.bias'] + + h_or = heaviside(inputs @ w_or.view(pop_size, -1).T + b_or.view(pop_size)) + h_nand = heaviside(inputs @ w_nand.view(pop_size, -1).T + b_nand.view(pop_size)) + hidden = torch.stack([h_or, h_nand], dim=-1) + + w2 = pop[f'{prefix}.layer2.weight'] + b2 = pop[f'{prefix}.layer2.bias'] + out = heaviside((hidden * w2.view(pop_size, 2)).sum(-1) + b2.view(pop_size)) + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i, (inp, exp, got) in enumerate(zip(inputs, expected, out[:, 0])): + if exp.item() != got.item(): + failures.append((inp.tolist(), exp.item(), got.item())) + + self._record(prefix, int(correct[0].item()), len(expected), failures) + return correct + + def _test_boolean_gates(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test all boolean gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== BOOLEAN GATES ===") + + # Single-layer gates + for gate in ['and', 'or', 'nand', 'nor', 'implies']: + scores += self._test_single_gate(pop, f'boolean.{gate}', self.tt2, self.expected[gate]) + total += 4 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + # NOT gate + w = pop['boolean.not.weight'] + b = pop['boolean.not.bias'] + out = heaviside(self.not_inputs @ w.view(pop_size, -1).T + b.view(pop_size)) + correct = (out == self.expected['not'].unsqueeze(1)).float().sum(0) + scores += correct + total += 2 + + failures = [] + if pop_size == 1: + for inp, exp, got in zip(self.not_inputs, self.expected['not'], out[:, 0]): + if exp.item() != got.item(): + failures.append((inp.tolist(), exp.item(), got.item())) + self._record('boolean.not', int(correct[0].item()), 2, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + # Two-layer gates + for gate in ['xnor', 'biimplies']: + scores += self._test_twolayer_gate(pop, f'boolean.{gate}', self.tt2, self.expected.get(gate, self.expected['xnor'])) + total += 4 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + # XOR with neuron1/neuron2 naming (same as xnor/biimplies) + scores += self._test_twolayer_gate(pop, 'boolean.xor', self.tt2, self.expected['xor']) + total += 4 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return scores, total + + # ========================================================================= + # ARITHMETIC - ADDERS + # ========================================================================= + + def _eval_xor(self, pop: Dict, prefix: str, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """Evaluate XOR gate with or/nand decomposition. + + Args: + a, b: Tensors of shape [num_tests] or [num_tests, pop_size] + + Returns: + Tensor of shape [num_tests, pop_size] + """ + pop_size = next(iter(pop.values())).shape[0] + + # Ensure inputs are [num_tests, pop_size] + if a.dim() == 1: + a = a.unsqueeze(1).expand(-1, pop_size) + if b.dim() == 1: + b = b.unsqueeze(1).expand(-1, pop_size) + + # inputs: [num_tests, pop_size, 2] + inputs = torch.stack([a, b], dim=-1) + + w_or = pop[f'{prefix}.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{prefix}.layer1.nand.bias'].view(pop_size) + + # [num_tests, pop_size] + h_or = heaviside((inputs * w_or).sum(-1) + b_or) + h_nand = heaviside((inputs * w_nand).sum(-1) + b_nand) + + # hidden: [num_tests, pop_size, 2] + hidden = torch.stack([h_or, h_nand], dim=-1) + + w2 = pop[f'{prefix}.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{prefix}.layer2.bias'].view(pop_size) + return heaviside((hidden * w2).sum(-1) + b2) + + def _eval_single_fa(self, pop: Dict, prefix: str, + a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """Evaluate single full adder. + + Args: + a, b, cin: Tensors of shape [num_tests] or [num_tests, pop_size] + + Returns: + sum_out, cout: Both of shape [num_tests, pop_size] + """ + pop_size = next(iter(pop.values())).shape[0] + + # Ensure inputs are [num_tests, pop_size] + if a.dim() == 1: + a = a.unsqueeze(1).expand(-1, pop_size) + if b.dim() == 1: + b = b.unsqueeze(1).expand(-1, pop_size) + if cin.dim() == 1: + cin = cin.unsqueeze(1).expand(-1, pop_size) + + # Half adder 1: a XOR b -> [num_tests, pop_size] + ha1_sum = self._eval_xor(pop, f'{prefix}.ha1.sum', a, b) + + # Half adder 1 carry: a AND b + ab = torch.stack([a, b], dim=-1) # [num_tests, pop_size, 2] + w_c1 = pop[f'{prefix}.ha1.carry.weight'].view(pop_size, 2) + b_c1 = pop[f'{prefix}.ha1.carry.bias'].view(pop_size) + ha1_carry = heaviside((ab * w_c1).sum(-1) + b_c1) + + # Half adder 2: ha1_sum XOR cin + ha2_sum = self._eval_xor(pop, f'{prefix}.ha2.sum', ha1_sum, cin) + + # Half adder 2 carry + sc = torch.stack([ha1_sum, cin], dim=-1) + w_c2 = pop[f'{prefix}.ha2.carry.weight'].view(pop_size, 2) + b_c2 = pop[f'{prefix}.ha2.carry.bias'].view(pop_size) + ha2_carry = heaviside((sc * w_c2).sum(-1) + b_c2) + + # Carry out: ha1_carry OR ha2_carry + carries = torch.stack([ha1_carry, ha2_carry], dim=-1) + w_cout = pop[f'{prefix}.carry_or.weight'].view(pop_size, 2) + b_cout = pop[f'{prefix}.carry_or.bias'].view(pop_size) + cout = heaviside((carries * w_cout).sum(-1) + b_cout) + + return ha2_sum, cout + + def _test_halfadder(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test half adder.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== HALF ADDER ===") + + # Sum (XOR) + scores += self._test_xor_ornand(pop, 'arithmetic.halfadder.sum', self.tt2, self.expected['ha_sum']) + total += 4 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + # Carry (AND) + scores += self._test_single_gate(pop, 'arithmetic.halfadder.carry', self.tt2, self.expected['ha_carry']) + total += 4 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return scores, total + + def _test_fulladder(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test full adder with all 8 input combinations.""" + pop_size = next(iter(pop.values())).shape[0] + + if debug: + print("\n=== FULL ADDER ===") + + a = self.tt3[:, 0] + b = self.tt3[:, 1] + cin = self.tt3[:, 2] + + sum_out, cout = self._eval_single_fa(pop, 'arithmetic.fulladder', a, b, cin) + + sum_correct = (sum_out == self.expected['fa_sum'].unsqueeze(1)).float().sum(0) + cout_correct = (cout == self.expected['fa_cout'].unsqueeze(1)).float().sum(0) + + failures_sum = [] + failures_cout = [] + if pop_size == 1: + for i in range(8): + if sum_out[i, 0].item() != self.expected['fa_sum'][i].item(): + failures_sum.append(([a[i].item(), b[i].item(), cin[i].item()], + self.expected['fa_sum'][i].item(), sum_out[i, 0].item())) + if cout[i, 0].item() != self.expected['fa_cout'][i].item(): + failures_cout.append(([a[i].item(), b[i].item(), cin[i].item()], + self.expected['fa_cout'][i].item(), cout[i, 0].item())) + + self._record('arithmetic.fulladder.sum', int(sum_correct[0].item()), 8, failures_sum) + self._record('arithmetic.fulladder.cout', int(cout_correct[0].item()), 8, failures_cout) + + if debug: + for r in self.results[-2:]: + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return sum_correct + cout_correct, 16 + + def _test_ripplecarry(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit ripple carry adder.""" + pop_size = next(iter(pop.values())).shape[0] + + if debug: + print(f"\n=== RIPPLE CARRY {bits}-BIT ===") + + prefix = f'arithmetic.ripplecarry{bits}bit' + max_val = 1 << bits + num_tests = min(max_val * max_val, 65536) + + if bits <= 4: + # Exhaustive for small widths + test_a = torch.arange(max_val, device=self.device) + test_b = torch.arange(max_val, device=self.device) + a_vals, b_vals = torch.meshgrid(test_a, test_b, indexing='ij') + a_vals = a_vals.flatten() + b_vals = b_vals.flatten() + elif bits == 8: + # Strategic sampling for 8-bit + edge_vals = [0, 1, 2, 127, 128, 254, 255] + pairs = [(a, b) for a in edge_vals for b in edge_vals] + for i in range(0, 256, 16): + pairs.append((i, 255 - i)) + pairs = list(set(pairs)) + a_vals = torch.tensor([p[0] for p in pairs], device=self.device) + b_vals = torch.tensor([p[1] for p in pairs], device=self.device) + num_tests = len(pairs) + else: + # Width-scaled edges and bit patterns so the high full adders see + # ones and carries, not just the low byte. + half = 1 << (bits - 1) + top = max_val - 1 + alt_a = int('AA' * (bits // 8), 16) + alt_5 = int('55' * (bits // 8), 16) + edge_vals = [0, 1, 2, 255, 256, half - 1, half, top - 1, top, alt_a, alt_5] + if bits == 32: + edge_vals += [0xFFFF, 0x10000, 0xDEADBEEF, 0x12345678, 1_000_000_000] + pairs = [(a, b) for a in edge_vals for b in edge_vals] + step = max_val // 16 + for i in range(0, 16): + v = i * step + pairs.append((v, top - v)) + pairs = list(set(pairs)) + a_vals = torch.tensor([p[0] for p in pairs], device=self.device) + b_vals = torch.tensor([p[1] for p in pairs], device=self.device) + num_tests = len(pairs) + + # Convert to bits [num_tests, bits] + a_bits = torch.stack([((a_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + b_bits = torch.stack([((b_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + + # Evaluate ripple carry + carry = torch.zeros(len(a_vals), pop_size, device=self.device) + sum_bits = [] + + for bit in range(bits): + bit_idx = bits - 1 - bit # LSB first + s, carry = self._eval_single_fa( + pop, f'{prefix}.fa{bit}', + a_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + b_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + carry + ) + sum_bits.append(s) + + # Reconstruct result. float64 keeps 32-bit values exact; float32 + # would round above 2^24 and corrupt the comparison. + sum_bits = torch.stack(sum_bits[::-1], dim=-1) # MSB first + result = torch.zeros(len(a_vals), pop_size, device=self.device, dtype=torch.float64) + for i in range(bits): + result += sum_bits[:, :, i].double() * (1 << (bits - 1 - i)) + + # Expected + expected = ((a_vals + b_vals) & (max_val - 1)).unsqueeze(1).expand(-1, pop_size).double() + correct = (result == expected).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(min(len(a_vals), 100)): + if result[i, 0].item() != expected[i, 0].item(): + failures.append(( + [int(a_vals[i].item()), int(b_vals[i].item())], + int(expected[i, 0].item()), + int(result[i, 0].item()) + )) + + self._record(prefix, int(correct[0].item()), num_tests, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, num_tests + + # ========================================================================= + # 3-OPERAND ADDER + # ========================================================================= + + def _test_add3(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test 3-operand 8-bit adder (A + B + C).""" + pop_size = next(iter(pop.values())).shape[0] + + if debug: + print(f"\n=== 3-OPERAND ADDER ===") + + prefix = 'arithmetic.add3_8bit' + bits = 8 + + # Strategic test cases for 3-operand addition + # Include edge cases and overflow scenarios + test_cases = [] + # Small values + for a in [0, 1, 2]: + for b in [0, 1, 2]: + for c in [0, 1, 2]: + test_cases.append((a, b, c)) + # Edge values + edge = [0, 1, 127, 128, 254, 255] + for a in edge: + for b in edge: + for c in edge: + test_cases.append((a, b, c)) + # Specific multi-operand expression tests + test_cases.extend([ + (15, 27, 33), # Example from roadmap: 15 + 27 + 33 = 75 + (100, 100, 55), # = 255 (exact fit) + (100, 100, 56), # = 256 -> 0 (overflow) + (85, 85, 85), # = 255 (exact fit) + (86, 85, 85), # = 256 -> 0 (overflow) + ]) + test_cases = list(set(test_cases)) + + a_vals = torch.tensor([t[0] for t in test_cases], device=self.device) + b_vals = torch.tensor([t[1] for t in test_cases], device=self.device) + c_vals = torch.tensor([t[2] for t in test_cases], device=self.device) + num_tests = len(test_cases) + + # Convert to bits [num_tests, bits] MSB-first + a_bits = torch.stack([((a_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + b_bits = torch.stack([((b_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + c_bits = torch.stack([((c_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + + # Stage 1: A + B + carry1 = torch.zeros(num_tests, pop_size, device=self.device) + stage1_bits = [] + for bit in range(bits): + bit_idx = bits - 1 - bit # LSB first + s, carry1 = self._eval_single_fa( + pop, f'{prefix}.stage1.fa{bit}', + a_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + b_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + carry1 + ) + stage1_bits.append(s) + + # Stage 2: stage1_result + C + carry2 = torch.zeros(num_tests, pop_size, device=self.device) + result_bits = [] + for bit in range(bits): + bit_idx = bits - 1 - bit # LSB first + s, carry2 = self._eval_single_fa( + pop, f'{prefix}.stage2.fa{bit}', + stage1_bits[bit], # Already [num_tests, pop_size] + c_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + carry2 + ) + result_bits.append(s) + + # Reconstruct result (bits are in LSB-first order, need to reverse for MSB-first) + result_bits = torch.stack(result_bits[::-1], dim=-1) # MSB first + result = torch.zeros(num_tests, pop_size, device=self.device) + for i in range(bits): + result += result_bits[:, :, i] * (1 << (bits - 1 - i)) + + # Expected (8-bit wrap) + expected = ((a_vals + b_vals + c_vals) & 0xFF).unsqueeze(1).expand(-1, pop_size).float() + correct = (result == expected).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(min(num_tests, 100)): + if result[i, 0].item() != expected[i, 0].item(): + failures.append(( + [int(a_vals[i].item()), int(b_vals[i].item()), int(c_vals[i].item())], + int(expected[i, 0].item()), + int(result[i, 0].item()) + )) + + self._record(prefix, int(correct[0].item()), num_tests, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + if failures: + for inp, exp, got in failures[:5]: + print(f" FAIL: {inp[0]} + {inp[1]} + {inp[2]} = {exp}, got {got}") + + return correct, num_tests + + # ========================================================================= + # ORDER OF OPERATIONS (A + B × C) + # ========================================================================= + + def _test_expr_add_mul(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test A + B × C expression circuit (order of operations).""" + pop_size = next(iter(pop.values())).shape[0] + + if debug: + print(f"\n=== ORDER OF OPERATIONS (A + B × C) ===") + + prefix = 'arithmetic.expr_add_mul' + bits = 8 + + # Test cases for order of operations + test_cases = [] + + # Specific examples from roadmap + test_cases.extend([ + (5, 3, 2), # 5 + 3 × 2 = 5 + 6 = 11 + (10, 4, 3), # 10 + 4 × 3 = 10 + 12 = 22 + (1, 10, 10), # 1 + 10 × 10 = 1 + 100 = 101 + (0, 15, 17), # 0 + 15 × 17 = 255 + (1, 15, 17), # 1 + 15 × 17 = 256 -> 0 (overflow) + (100, 5, 5), # 100 + 5 × 5 = 100 + 25 = 125 + ]) + + # Edge cases + test_cases.extend([ + (0, 0, 0), # 0 + 0 × 0 = 0 + (255, 0, 0), # 255 + 0 × 0 = 255 + (0, 255, 1), # 0 + 255 × 1 = 255 + (0, 1, 255), # 0 + 1 × 255 = 255 + (1, 1, 1), # 1 + 1 × 1 = 2 + (0, 16, 16), # 0 + 16 × 16 = 256 -> 0 (overflow) + ]) + + # Systematic small values + for a in [0, 1, 5, 10]: + for b in [0, 1, 2, 3]: + for c in [0, 1, 2, 3]: + test_cases.append((a, b, c)) + + # Remove duplicates + test_cases = list(set(test_cases)) + + a_vals = torch.tensor([t[0] for t in test_cases], device=self.device) + b_vals = torch.tensor([t[1] for t in test_cases], device=self.device) + c_vals = torch.tensor([t[2] for t in test_cases], device=self.device) + num_tests = len(test_cases) + + # Convert to bits [num_tests, bits] MSB-first + a_bits = torch.stack([((a_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + b_bits = torch.stack([((b_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + c_bits = torch.stack([((c_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + + # Evaluate mask stage: mask[stage][bit] = B[bit] AND C[stage] + # In the circuit: mask.s[stage].b[bit] operates on positional bits + # stage 0 = LSB of C (c_bits[:, 7]), stage 7 = MSB of C (c_bits[:, 0]) + # bit 0 = LSB of B (b_bits[:, 7]), bit 7 = MSB of B (b_bits[:, 0]) + masks = torch.zeros(8, num_tests, pop_size, 8, device=self.device) # [stage, tests, pop, bits] + for stage in range(8): + c_stage_bit = c_bits[:, 7 - stage].unsqueeze(1).expand(-1, pop_size) # C[stage] + for bit in range(8): + b_bit_val = b_bits[:, 7 - bit].unsqueeze(1).expand(-1, pop_size) # B[bit] + # AND gate + w = pop.get(f'{prefix}.mul.mask.s{stage}.b{bit}.weight') + bias = pop.get(f'{prefix}.mul.mask.s{stage}.b{bit}.bias') + if w is not None and bias is not None: + w = w.squeeze(-1) # [pop] + b_tensor = bias.squeeze(-1) # [pop] + # Properly broadcast for batch evaluation + inp = torch.stack([b_bit_val, c_stage_bit], dim=-1) # [tests, pop, 2] + out = heaviside(torch.einsum('tpi,pi->tp', inp, w) + b_tensor) + masks[stage, :, :, bit] = out + + # Accumulator stages + # acc[0] = mask[0] (no shift) + # acc[1] = acc[0] + (mask[1] << 1) + # ... + # acc[7] = acc[6] + (mask[7] << 7) + acc = masks[0].clone() # [tests, pop, 8] - start with mask[0] + + for stage in range(1, 8): + # Create shifted mask: (mask[stage] << stage) + # Shift left by 'stage' positions: bits 0..stage-1 become 0, bit k becomes mask[stage][k-stage] + shifted_mask = torch.zeros(num_tests, pop_size, 8, device=self.device) + for bit in range(8): + if bit >= stage: + shifted_mask[:, :, bit] = masks[stage, :, :, bit - stage] + # else: remains 0 + + # Add acc + shifted_mask using full adders + carry = torch.zeros(num_tests, pop_size, device=self.device) + new_acc = torch.zeros(num_tests, pop_size, 8, device=self.device) + for bit in range(8): + s, carry = self._eval_single_fa( + pop, f'{prefix}.mul.acc.s{stage}.fa{bit}', + acc[:, :, bit], + shifted_mask[:, :, bit], + carry + ) + new_acc[:, :, bit] = s + acc = new_acc + + # Final add stage: A + acc (multiplication result) + carry = torch.zeros(num_tests, pop_size, device=self.device) + result_bits = [] + for bit in range(8): + a_bit_val = a_bits[:, 7 - bit].unsqueeze(1).expand(-1, pop_size) + s, carry = self._eval_single_fa( + pop, f'{prefix}.add.fa{bit}', + a_bit_val, + acc[:, :, bit], + carry + ) + result_bits.append(s) + + # Reconstruct result + result_bits = torch.stack(result_bits[::-1], dim=-1) # MSB first + result = torch.zeros(num_tests, pop_size, device=self.device) + for i in range(bits): + result += result_bits[:, :, i] * (1 << (bits - 1 - i)) + + # Expected: A + (B × C), with 8-bit wrap + expected = ((a_vals + b_vals * c_vals) & 0xFF).unsqueeze(1).expand(-1, pop_size).float() + correct = (result == expected).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(min(num_tests, 100)): + if result[i, 0].item() != expected[i, 0].item(): + failures.append(( + [int(a_vals[i].item()), int(b_vals[i].item()), int(c_vals[i].item())], + int(expected[i, 0].item()), + int(result[i, 0].item()) + )) + + self._record(prefix, int(correct[0].item()), num_tests, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + if failures: + for inp, exp, got in failures[:5]: + print(f" FAIL: {inp[0]} + {inp[1]} × {inp[2]} = {exp}, got {got}") + + return correct, num_tests + + # ========================================================================= + # COMPARATORS + # ========================================================================= + + def _eval_bit_cascade_compare( + self, + pop: Dict, + cmp_prefix: str, + out_gt: str, + out_lt: str, + out_ge: str, + out_le: str, + out_eq: str, + bits: int, + a_bits_2d: torch.Tensor, + b_bits_2d: torch.Tensor, + ) -> Dict[str, torch.Tensor]: + """Walk the ternary bit-cascade comparator generated by + build.add_bit_cascade_compare. Returns a dict with gt/lt/ge/le/eq each of + shape [num_tests, pop_size]. a_bits_2d, b_bits_2d are [num_tests, bits] + MSB-first. + """ + pop_size = next(iter(pop.values())).shape[0] + + # Per-bit gt, lt, eq + gt_b: List[torch.Tensor] = [] + lt_b: List[torch.Tensor] = [] + eq_b: List[torch.Tensor] = [] + for i in range(bits): + a_i = a_bits_2d[:, i].unsqueeze(1).expand(-1, pop_size) + b_i = b_bits_2d[:, i].unsqueeze(1).expand(-1, pop_size) + ab = torch.stack([a_i, b_i], dim=-1) + + w = pop[f'{cmp_prefix}.bit{i}.gt.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.bit{i}.gt.bias'].view(pop_size) + gt_b.append(heaviside((ab * w).sum(-1) + bb)) + + w = pop[f'{cmp_prefix}.bit{i}.lt.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.bit{i}.lt.bias'].view(pop_size) + lt_b.append(heaviside((ab * w).sum(-1) + bb)) + + w = pop[f'{cmp_prefix}.bit{i}.eq.layer1.and.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.bit{i}.eq.layer1.and.bias'].view(pop_size) + h_and = heaviside((ab * w).sum(-1) + bb) + w = pop[f'{cmp_prefix}.bit{i}.eq.layer1.nor.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.bit{i}.eq.layer1.nor.bias'].view(pop_size) + h_nor = heaviside((ab * w).sum(-1) + bb) + hidden = torch.stack([h_and, h_nor], dim=-1) + w = pop[f'{cmp_prefix}.bit{i}.eq.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.bit{i}.eq.bias'].view(pop_size) + eq_b.append(heaviside((hidden * w).sum(-1) + bb)) + + # eq_prefix[i] = AND of eq[0..i-1] + eq_pref: List[Optional[torch.Tensor]] = [None] + for i in range(1, bits): + eq_stack = torch.stack(eq_b[:i], dim=-1) + w = pop[f'{cmp_prefix}.cascade.eq_prefix.bit{i}.weight'].view(pop_size, i) + bb = pop[f'{cmp_prefix}.cascade.eq_prefix.bit{i}.bias'].view(pop_size) + eq_pref.append(heaviside((eq_stack * w).sum(-1) + bb)) + + # cascade gt[i], lt[i] = eq_prefix[i] AND gt_b[i] / lt_b[i] + casc_gt = [gt_b[0]] + casc_lt = [lt_b[0]] + for i in range(1, bits): + inp = torch.stack([eq_pref[i], gt_b[i]], dim=-1) + w = pop[f'{cmp_prefix}.cascade.gt.bit{i}.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.cascade.gt.bit{i}.bias'].view(pop_size) + casc_gt.append(heaviside((inp * w).sum(-1) + bb)) + inp = torch.stack([eq_pref[i], lt_b[i]], dim=-1) + w = pop[f'{cmp_prefix}.cascade.lt.bit{i}.weight'].view(pop_size, 2) + bb = pop[f'{cmp_prefix}.cascade.lt.bit{i}.bias'].view(pop_size) + casc_lt.append(heaviside((inp * w).sum(-1) + bb)) + + # Final OR for GT / LT + gt_stack = torch.stack(casc_gt, dim=-1) + w = pop[f'{out_gt}.weight'].view(pop_size, bits) + bb = pop[f'{out_gt}.bias'].view(pop_size) + final_gt = heaviside((gt_stack * w).sum(-1) + bb) + + lt_stack = torch.stack(casc_lt, dim=-1) + w = pop[f'{out_lt}.weight'].view(pop_size, bits) + bb = pop[f'{out_lt}.bias'].view(pop_size) + final_lt = heaviside((lt_stack * w).sum(-1) + bb) + + # Final AND for EQ + eq_stack = torch.stack(eq_b, dim=-1) + w = pop[f'{out_eq}.weight'].view(pop_size, bits) + bb = pop[f'{out_eq}.bias'].view(pop_size) + final_eq = heaviside((eq_stack * w).sum(-1) + bb) + + # GE = NOT(LT) buffer pair, LE = NOT(GT) buffer pair + w = pop[f'{out_ge}.not_lt.weight'].view(pop_size) + bb = pop[f'{out_ge}.not_lt.bias'].view(pop_size) + not_lt = heaviside(final_lt * w + bb) + w = pop[f'{out_ge}.weight'].view(pop_size) + bb = pop[f'{out_ge}.bias'].view(pop_size) + final_ge = heaviside(not_lt * w + bb) + + w = pop[f'{out_le}.not_gt.weight'].view(pop_size) + bb = pop[f'{out_le}.not_gt.bias'].view(pop_size) + not_gt = heaviside(final_gt * w + bb) + w = pop[f'{out_le}.weight'].view(pop_size) + bb = pop[f'{out_le}.bias'].view(pop_size) + final_le = heaviside(not_gt * w + bb) + + return { + "gt": final_gt, "lt": final_lt, "eq": final_eq, + "ge": final_ge, "le": final_le, + } + + def _test_comparators(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test 8-bit comparators (bit-cascade).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== COMPARATORS (8-bit bit-cascade) ===") + + bits = 8 + a_bits = torch.stack([((self.comp_a >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + b_bits = torch.stack([((self.comp_b >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + try: + outs = self._eval_bit_cascade_compare( + pop, + f"arithmetic.cmp{bits}bit", + f"arithmetic.greaterthan{bits}bit", + f"arithmetic.lessthan{bits}bit", + f"arithmetic.greaterorequal{bits}bit", + f"arithmetic.lessorequal{bits}bit", + f"arithmetic.equality{bits}bit", + bits, + a_bits, + b_bits, + ) + except KeyError: + return scores, total + + for kind, op in [ + ("gt", lambda a, b: a > b), + ("lt", lambda a, b: a < b), + ("ge", lambda a, b: a >= b), + ("le", lambda a, b: a <= b), + ("eq", lambda a, b: a == b), + ]: + expected = torch.tensor( + [1.0 if op(a.item(), b.item()) else 0.0 for a, b in zip(self.comp_a, self.comp_b)], + device=self.device, + ) + out = outs[kind] + correct = (out == expected.unsqueeze(1)).float().sum(0) + scores += correct + total += len(self.comp_a) + name_map = { + "gt": f"arithmetic.greaterthan{bits}bit", + "lt": f"arithmetic.lessthan{bits}bit", + "ge": f"arithmetic.greaterorequal{bits}bit", + "le": f"arithmetic.lessorequal{bits}bit", + "eq": f"arithmetic.equality{bits}bit", + } + failures = [] + if pop_size == 1: + for i in range(len(self.comp_a)): + if out[i, 0].item() != expected[i].item(): + failures.append(( + [int(self.comp_a[i].item()), int(self.comp_b[i].item())], + expected[i].item(), + out[i, 0].item(), + )) + self._record(name_map[kind], int(correct[0].item()), len(self.comp_a), failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + return scores, total + + def _test_comparators_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit comparator circuits (GT, LT, GE, LE, EQ) via bit-cascade.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {bits}-BIT COMPARATORS (bit-cascade) ===") + + if bits == 32: + comp_a = self.comp32_a + comp_b = self.comp32_b + elif bits == 16: + comp_a = self.comp_a.clamp(0, 65535) + comp_b = self.comp_b.clamp(0, 65535) + else: + comp_a = self.comp_a + comp_b = self.comp_b + + num_tests = len(comp_a) + a_bits = torch.stack([((comp_a >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + b_bits = torch.stack([((comp_b >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + + try: + outs = self._eval_bit_cascade_compare( + pop, + f"arithmetic.cmp{bits}bit", + f"arithmetic.greaterthan{bits}bit", + f"arithmetic.lessthan{bits}bit", + f"arithmetic.greaterorequal{bits}bit", + f"arithmetic.lessorequal{bits}bit", + f"arithmetic.equality{bits}bit", + bits, + a_bits, + b_bits, + ) + except KeyError: + return scores, total + + for kind, op in [ + ("gt", lambda a, b: a > b), + ("lt", lambda a, b: a < b), + ("ge", lambda a, b: a >= b), + ("le", lambda a, b: a <= b), + ("eq", lambda a, b: a == b), + ]: + expected = torch.tensor( + [1.0 if op(a.item(), b.item()) else 0.0 for a, b in zip(comp_a, comp_b)], + device=self.device, + ) + out = outs[kind] + correct = (out == expected.unsqueeze(1)).float().sum(0) + scores += correct + total += num_tests + name_map = { + "gt": f"arithmetic.greaterthan{bits}bit", + "lt": f"arithmetic.lessthan{bits}bit", + "ge": f"arithmetic.greaterorequal{bits}bit", + "le": f"arithmetic.lessorequal{bits}bit", + "eq": f"arithmetic.equality{bits}bit", + } + failures = [] + if pop_size == 1: + for i in range(num_tests): + if out[i, 0].item() != expected[i].item(): + failures.append(( + [int(comp_a[i].item()), int(comp_b[i].item())], + expected[i].item(), + out[i, 0].item(), + )) + self._record(name_map[kind], int(correct[0].item()), num_tests, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return scores, total + + def _test_subtractor_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit subtractor circuit (A - B).""" + pop_size = next(iter(pop.values())).shape[0] + + if debug: + print(f"\n=== {bits}-BIT SUBTRACTOR ===") + + prefix = f'arithmetic.sub{bits}bit' + max_val = 1 << bits + + if bits == 32: + test_pairs = [ + (1000, 500), (5000, 3000), (1000000, 500000), + (0xFFFFFFFF, 1), (0x80000000, 1), (100, 100), + (0, 0), (1, 0), (0, 1), (256, 255), + (0xDEADBEEF, 0xCAFEBABE), (1000000000, 999999999), + ] + else: + test_pairs = [(a, b) for a in [0, 1, 127, 128, 255] for b in [0, 1, 127, 128, 255]] + + a_vals = torch.tensor([p[0] for p in test_pairs], device=self.device, dtype=torch.long) + b_vals = torch.tensor([p[1] for p in test_pairs], device=self.device, dtype=torch.long) + num_tests = len(test_pairs) + + a_bits = torch.stack([((a_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + b_bits = torch.stack([((b_vals >> (bits - 1 - i)) & 1).float() for i in range(bits)], dim=1) + + not_b_bits = torch.zeros_like(b_bits) + for bit in range(bits): + w = pop[f'{prefix}.not_b.bit{bit}.weight'].view(pop_size, -1) + b = pop[f'{prefix}.not_b.bit{bit}.bias'].view(pop_size) + not_b_bits[:, bit] = heaviside(b_bits[:, bit:bit+1] @ w.T + b)[:, 0] + + carry = torch.ones(num_tests, pop_size, device=self.device) + sum_bits = [] + + for bit in range(bits): + bit_idx = bits - 1 - bit + s, carry = self._eval_single_fa( + pop, f'{prefix}.fa{bit}', + a_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + not_b_bits[:, bit_idx].unsqueeze(1).expand(-1, pop_size), + carry + ) + sum_bits.append(s) + + sum_bits = torch.stack(sum_bits[::-1], dim=-1) + result = torch.zeros(num_tests, pop_size, device=self.device, dtype=torch.float64) + for i in range(bits): + result += sum_bits[:, :, i].double() * (1 << (bits - 1 - i)) + + expected = ((a_vals - b_vals) & (max_val - 1)).unsqueeze(1).expand(-1, pop_size).double() + correct = (result == expected).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(min(num_tests, 20)): + if result[i, 0].item() != expected[i, 0].item(): + failures.append(( + [int(a_vals[i].item()), int(b_vals[i].item())], + int(expected[i, 0].item()), + int(result[i, 0].item()) + )) + + self._record(prefix, int(correct[0].item()), num_tests, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, num_tests + + def _test_bitwise_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit bitwise operations (AND, OR, XOR, NOT).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {bits}-BIT BITWISE OPS ===") + + if bits == 32: + test_pairs = [ + (0xAAAAAAAA, 0x55555555), (0xFFFFFFFF, 0x00000000), + (0x12345678, 0x87654321), (0xDEADBEEF, 0xCAFEBABE), + (0x0F0F0F0F, 0xF0F0F0F0), (0, 0), (0xFFFFFFFF, 0xFFFFFFFF), + ] + else: + test_pairs = [(0xAA, 0x55), (0xFF, 0x00), (0x0F, 0xF0)] + + a_vals = torch.tensor([p[0] for p in test_pairs], device=self.device, dtype=torch.long) + b_vals = torch.tensor([p[1] for p in test_pairs], device=self.device, dtype=torch.long) + num_tests = len(test_pairs) + + ops = [ + ('and', lambda a, b: a & b), + ('or', lambda a, b: a | b), + ('xor', lambda a, b: a ^ b), + ] + + for op_name, op_fn in ops: + try: + result_bits = [] + for bit in range(bits): + a_bit = ((a_vals >> (bits - 1 - bit)) & 1).float().unsqueeze(1).expand(-1, pop_size) + b_bit = ((b_vals >> (bits - 1 - bit)) & 1).float().unsqueeze(1).expand(-1, pop_size) + inp = torch.stack([a_bit, b_bit], dim=-1) # [tests, pop, 2] + + if op_name == 'xor': + prefix = f'alu.alu{bits}bit.{op_name}.bit{bit}' + w_or = pop[f'{prefix}.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{prefix}.layer1.nand.bias'].view(pop_size) + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + hidden = torch.stack([h_or, h_nand], dim=-1) + w2 = pop[f'{prefix}.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{prefix}.layer2.bias'].view(pop_size) + out = heaviside((hidden * w2).sum(-1) + b2) + else: + w = pop[f'alu.alu{bits}bit.{op_name}.bit{bit}.weight'].view(pop_size, 2) + b = pop[f'alu.alu{bits}bit.{op_name}.bit{bit}.bias'].view(pop_size) + out = heaviside((inp * w).sum(-1) + b) + + result_bits.append(out) # [tests, pop] + + results = torch.zeros(num_tests, pop_size, device=self.device, dtype=torch.float64) + for i in range(bits): + results += result_bits[i].double() * (1 << (bits - 1 - i)) + expected = torch.tensor([op_fn(a.item(), b.item()) for a, b in zip(a_vals, b_vals)], + device=self.device, dtype=torch.float64).unsqueeze(1) + + correct = (results == expected).float().sum(0) # [pop] + self._record(f'alu.alu{bits}bit.{op_name}', int(correct[0].item()), num_tests, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + scores += correct + total += num_tests + except KeyError as e: + if debug: + print(f" alu.alu{bits}bit.{op_name}: SKIP (missing {e})") + + try: + test_vals = a_vals + result_bits = [] + for bit in range(bits): + a_bit = ((test_vals >> (bits - 1 - bit)) & 1).float().unsqueeze(1).expand(-1, pop_size) + w = pop[f'alu.alu{bits}bit.not.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu{bits}bit.not.bit{bit}.bias'].view(pop_size) + result_bits.append(heaviside(a_bit * w + b)) + + results = torch.zeros(num_tests, pop_size, device=self.device, dtype=torch.float64) + for i in range(bits): + results += result_bits[i].double() * (1 << (bits - 1 - i)) + expected = torch.tensor([(~a.item()) & ((1 << bits) - 1) for a in test_vals], + device=self.device, dtype=torch.float64).unsqueeze(1) + + correct = (results == expected).float().sum(0) + self._record(f'alu.alu{bits}bit.not', int(correct[0].item()), num_tests, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + scores += correct + total += num_tests + except KeyError as e: + if debug: + print(f" alu.alu{bits}bit.not: SKIP (missing {e})") + + return scores, total + + def _test_shifts_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit shift operations (SHL, SHR).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {bits}-BIT SHIFTS ===") + + if bits == 32: + test_vals = [0x12345678, 0x80000001, 0x00000001, 0xFFFFFFFF, 0x55555555] + else: + test_vals = [0x81, 0x55, 0x01, 0xFF, 0xAA] + + a_vals = torch.tensor(test_vals, device=self.device, dtype=torch.long) + num_tests = len(test_vals) + max_val = (1 << bits) - 1 + + for op_name, op_fn in [('shl', lambda x: (x << 1) & max_val), ('shr', lambda x: x >> 1)]: + try: + result_bits = [] + for bit in range(bits): + w = pop[f'alu.alu{bits}bit.{op_name}.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu{bits}bit.{op_name}.bit{bit}.bias'].view(pop_size) + + if op_name == 'shl': + if bit < bits - 1: + src_bit = ((a_vals >> (bits - 2 - bit)) & 1).float() + else: + src_bit = torch.zeros(num_tests, device=self.device) + else: + if bit > 0: + src_bit = ((a_vals >> (bits - bit)) & 1).float() + else: + src_bit = torch.zeros(num_tests, device=self.device) + + out = heaviside(src_bit.unsqueeze(1) * w + b) # [tests, pop] + result_bits.append(out) + + results = torch.zeros(num_tests, pop_size, device=self.device, dtype=torch.float64) + for i in range(bits): + results += result_bits[i].double() * (1 << (bits - 1 - i)) + expected = torch.tensor([op_fn(a.item()) for a in a_vals], + device=self.device, dtype=torch.float64).unsqueeze(1) + + correct = (results == expected).float().sum(0) # [pop] + self._record(f'alu.alu{bits}bit.{op_name}', int(correct[0].item()), num_tests, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + scores += correct + total += num_tests + except KeyError as e: + if debug: + print(f" alu.alu{bits}bit.{op_name}: SKIP (missing {e})") + + return scores, total + + def _test_inc_dec_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit INC and DEC operations.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {bits}-BIT INC/DEC ===") + + if bits == 32: + test_vals = [0, 1, 0xFFFFFFFF, 0x7FFFFFFF, 0x80000000, 1000000, 0xFFFFFFFE] + else: + test_vals = [0, 1, 254, 255, 127, 128] + + a_vals = torch.tensor(test_vals, device=self.device, dtype=torch.long) + num_tests = len(test_vals) + max_val = (1 << bits) - 1 + + for op_name, op_fn in [('inc', lambda x: (x + 1) & max_val), ('dec', lambda x: (x - 1) & max_val)]: + try: + carry = torch.ones(num_tests, pop_size, device=self.device) + result_bits = [] + + for bit in range(bits): + a_bit = ((a_vals >> bit) & 1).float().unsqueeze(1).expand(-1, pop_size) + + prefix = f'alu.alu{bits}bit.{op_name}.bit{bit}' + w_or = pop[f'{prefix}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{prefix}.xor.layer1.nand.bias'].view(pop_size) + + inp = torch.stack([a_bit, carry], dim=-1) # [tests, pop, 2] + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + + w2 = pop[f'{prefix}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{prefix}.xor.layer2.bias'].view(pop_size) + hidden = torch.stack([h_or, h_nand], dim=-1) + xor_out = heaviside((hidden * w2).sum(-1) + b2) + result_bits.append(xor_out) + + if op_name == 'inc': + w_carry = pop[f'{prefix}.carry.weight'].view(pop_size, 2) + b_carry = pop[f'{prefix}.carry.bias'].view(pop_size) + carry = heaviside((inp * w_carry).sum(-1) + b_carry) + else: + w_not = pop[f'{prefix}.not_a.weight'].view(pop_size) + b_not = pop[f'{prefix}.not_a.bias'].view(pop_size) + not_a = heaviside(a_bit * w_not + b_not) + w_borrow = pop[f'{prefix}.borrow.weight'].view(pop_size, 2) + b_borrow = pop[f'{prefix}.borrow.bias'].view(pop_size) + binp = torch.stack([not_a, carry], dim=-1) + carry = heaviside((binp * w_borrow).sum(-1) + b_borrow) + + # result_bits[bit] is [tests, pop]; bit index is LSB-first + results = torch.zeros(num_tests, pop_size, device=self.device, dtype=torch.float64) + for bit in range(bits): + results += result_bits[bit].double() * (1 << bit) + expected = torch.tensor([op_fn(a.item()) for a in a_vals], + device=self.device, dtype=torch.float64).unsqueeze(1) + + correct = (results == expected).float().sum(0) # [pop] + self._record(f'alu.alu{bits}bit.{op_name}', int(correct[0].item()), num_tests, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + scores += correct + total += num_tests + except KeyError as e: + if debug: + print(f" alu.alu{bits}bit.{op_name}: SKIP (missing {e})") + + return scores, total + + def _test_neg_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit NEG operation (two's complement negation).""" + pop_size = next(iter(pop.values())).shape[0] + + if debug: + print(f"\n=== {bits}-BIT NEG ===") + + if bits == 32: + test_vals = [0, 1, 0xFFFFFFFF, 0x7FFFFFFF, 0x80000000, 1000, 1000000] + else: + test_vals = [0, 1, 127, 128, 255, 100] + + a_vals = torch.tensor(test_vals, device=self.device, dtype=torch.long) + num_tests = len(test_vals) + max_val = (1 << bits) - 1 + + try: + not_bits = [] + for bit in range(bits): + a_bit = ((a_vals >> bit) & 1).float().unsqueeze(1).expand(-1, pop_size) + w = pop[f'alu.alu{bits}bit.neg.not.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu{bits}bit.neg.not.bit{bit}.bias'].view(pop_size) + not_bits.append(heaviside(a_bit * w + b)) + + carry = torch.ones(num_tests, pop_size, device=self.device) + result_bits = [] + + for bit in range(bits): + prefix = f'alu.alu{bits}bit.neg.inc.bit{bit}' + not_bit = not_bits[bit] + + w_or = pop[f'{prefix}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{prefix}.xor.layer1.nand.bias'].view(pop_size) + + inp = torch.stack([not_bit, carry], dim=-1) # [tests, pop, 2] + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + + w2 = pop[f'{prefix}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{prefix}.xor.layer2.bias'].view(pop_size) + hidden = torch.stack([h_or, h_nand], dim=-1) + xor_out = heaviside((hidden * w2).sum(-1) + b2) + result_bits.append(xor_out) + + w_carry = pop[f'{prefix}.carry.weight'].view(pop_size, 2) + b_carry = pop[f'{prefix}.carry.bias'].view(pop_size) + carry = heaviside((inp * w_carry).sum(-1) + b_carry) + + results = torch.zeros(num_tests, pop_size, device=self.device, dtype=torch.float64) + for bit in range(bits): + results += result_bits[bit].double() * (1 << bit) + expected = torch.tensor([(-a.item()) & max_val for a in a_vals], + device=self.device, dtype=torch.float64).unsqueeze(1) + + correct = (results == expected).float().sum(0) # [pop] + self._record(f'alu.alu{bits}bit.neg', int(correct[0].item()), num_tests, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, num_tests + except KeyError as e: + if debug: + print(f" alu.alu{bits}bit.neg: SKIP (missing {e})") + return torch.zeros(pop_size, device=self.device), 0 + + # ========================================================================= + # THRESHOLD GATES + # ========================================================================= + + def _test_threshold_kofn(self, pop: Dict, k: int, name: str, debug: bool) -> Tuple[torch.Tensor, int]: + """Test k-of-n threshold gate.""" + pop_size = next(iter(pop.values())).shape[0] + prefix = f'threshold.{name}' + + # Test all 256 8-bit patterns + inputs = self.test_8bit_bits if len(self.test_8bit_bits) == 24 else None + if inputs is None: + test_vals = torch.arange(256, device=self.device, dtype=torch.long) + inputs = torch.stack([((test_vals >> (7 - i)) & 1).float() for i in range(8)], dim=1) + + # For k-of-8: output 1 if popcount >= k (for "at least k") + # For exact naming like "oneoutof8", it's exactly k=1 + popcounts = inputs.sum(dim=1) + + if 'atleast' in name: + expected = (popcounts >= k).float() + elif 'atmost' in name or 'minority' in name: + # minority = popcount <= 3 (less than half of 8) + expected = (popcounts <= k).float() + elif 'exactly' in name: + expected = (popcounts == k).float() + else: + # Standard k-of-n (at least k), including majority (>= 5) + expected = (popcounts >= k).float() + + w = pop[f'{prefix}.weight'] + b = pop[f'{prefix}.bias'] + out = heaviside(inputs @ w.view(pop_size, -1).T + b.view(pop_size)) + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(min(len(inputs), 256)): + if out[i, 0].item() != expected[i].item(): + val = int(sum(inputs[i, j].item() * (1 << (7 - j)) for j in range(8))) + failures.append((val, expected[i].item(), out[i, 0].item())) + + self._record(prefix, int(correct[0].item()), len(inputs), failures[:10]) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, len(inputs) + + def _test_threshold_gates(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test all threshold gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== THRESHOLD GATES ===") + + # k-of-8 gates + kofn_gates = [ + (1, 'oneoutof8'), (2, 'twooutof8'), (3, 'threeoutof8'), (4, 'fouroutof8'), + (5, 'fiveoutof8'), (6, 'sixoutof8'), (7, 'sevenoutof8'), (8, 'alloutof8'), + ] + + for k, name in kofn_gates: + try: + s, t = self._test_threshold_kofn(pop, k, name, debug) + scores += s + total += t + except KeyError: + pass + + # Special gates + special = [ + (5, 'majority'), (3, 'minority'), + (4, 'atleastk_4'), (4, 'atmostk_4'), (4, 'exactlyk_4'), + ] + + for k, name in special: + try: + s, t = self._test_threshold_kofn(pop, k, name, debug) + scores += s + total += t + except KeyError: + pass + + return scores, total + + # ========================================================================= + # MODULAR ARITHMETIC + # ========================================================================= + + def _test_modular(self, pop: Dict, mod: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test modular divisibility circuit. + + Two structures: mod 3/5/6/7/9/10/11/12 use bit-cascade equality + per multiple of N (`{prefix}.eq.k{k}.*` + final OR at `{prefix}`). + mod 2/4/8 use a single-layer ternary detector at `{prefix}` directly. + """ + pop_size = next(iter(pop.values())).shape[0] + prefix = f'modular.mod{mod}' + + inputs = torch.stack([((self.mod_test >> (7 - i)) & 1).float() for i in range(8)], dim=1) + expected = ((self.mod_test % mod) == 0).float() + out = None + + # Bit-cascade equality structure (non-power-of-2 moduli) + multiples = list(range(0, 256, mod)) + if (multiples + and f'{prefix}.eq.k{multiples[0]}.all.weight' in pop + and f'{prefix}.weight' in pop): + try: + match_outputs = [] + for k in multiples: + per_bit = [] + for i in range(8): + bit_in = inputs[:, i].unsqueeze(1).expand(-1, pop_size) + w = pop[f'{prefix}.eq.k{k}.bit{i}.match.weight'].view(pop_size) + b = pop[f'{prefix}.eq.k{k}.bit{i}.match.bias'].view(pop_size) + per_bit.append(heaviside(bit_in * w + b)) + stacked = torch.stack(per_bit, dim=-1) + w_and = pop[f'{prefix}.eq.k{k}.all.weight'].view(pop_size, 8) + b_and = pop[f'{prefix}.eq.k{k}.all.bias'].view(pop_size) + match_outputs.append(heaviside((stacked * w_and).sum(-1) + b_and)) + or_in = torch.stack(match_outputs, dim=-1) + w_or = pop[f'{prefix}.weight'].view(pop_size, len(multiples)) + b_or = pop[f'{prefix}.bias'].view(pop_size) + out = heaviside((or_in * w_or).sum(-1) + b_or) + except (KeyError, RuntimeError): + out = None + + # Single-layer ternary detector (powers of 2) + if out is None: + try: + w = pop[f'{prefix}.weight'] + b = pop[f'{prefix}.bias'] + out = heaviside(inputs @ w.view(pop_size, -1).T + b.view(pop_size)) + except (KeyError, RuntimeError): + return torch.zeros(pop_size, device=self.device), 0 + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(256): + if out[i, 0].item() != expected[i].item(): + failures.append((i, expected[i].item(), out[i, 0].item())) + + self._record(prefix, int(correct[0].item()), 256, failures[:10]) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, 256 + + def _test_modular_all(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test all modular arithmetic circuits.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== MODULAR ARITHMETIC ===") + + for mod in range(2, 13): + s, t = self._test_modular(pop, mod, debug) + scores += s + total += t + + return scores, total + + # ========================================================================= + # PATTERN RECOGNITION + # ========================================================================= + + def _test_pattern(self, pop: Dict, name: str, expected_fn: Callable[[int], float], + debug: bool) -> Tuple[torch.Tensor, int]: + """Test pattern recognition circuit.""" + pop_size = next(iter(pop.values())).shape[0] + prefix = f'pattern_recognition.{name}' + + test_vals = torch.arange(256, device=self.device, dtype=torch.long) + inputs = torch.stack([((test_vals >> (7 - i)) & 1).float() for i in range(8)], dim=1) + expected = torch.tensor([expected_fn(v.item()) for v in test_vals], device=self.device) + + try: + w = pop[f'{prefix}.weight'].view(pop_size, -1) + b = pop[f'{prefix}.bias'].view(pop_size) + out = heaviside(inputs @ w.T + b) + except KeyError: + return torch.zeros(pop_size, device=self.device), 0 + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(256): + if out[i, 0].item() != expected[i].item(): + failures.append((i, expected[i].item(), out[i, 0].item())) + + self._record(prefix, int(correct[0].item()), 256, failures[:10]) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, 256 + + def _test_patterns(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test pattern recognition circuits.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== PATTERN RECOGNITION ===") + + # Use correct naming: pattern_recognition.allzeros, pattern_recognition.allones + patterns = [ + ('allzeros', lambda v: 1.0 if v == 0 else 0.0), + ('allones', lambda v: 1.0 if v == 255 else 0.0), + ] + + for name, fn in patterns: + s, t = self._test_pattern(pop, name, fn, debug) + scores += s + total += t + + return scores, total + + # ========================================================================= + # ERROR DETECTION + # ========================================================================= + + def _eval_xor_tree_stage(self, pop: Dict, prefix: str, stage: int, idx: int, + a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """Evaluate a single XOR in the parity tree.""" + pop_size = next(iter(pop.values())).shape[0] + xor_prefix = f'{prefix}.stage{stage}.xor{idx}' + + # Ensure 2D: [256, pop_size] + if a.dim() == 1: + a = a.unsqueeze(1).expand(-1, pop_size) + if b.dim() == 1: + b = b.unsqueeze(1).expand(-1, pop_size) + + # Layer 1: OR and NAND + w_or = pop[f'{xor_prefix}.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{xor_prefix}.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{xor_prefix}.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{xor_prefix}.layer1.nand.bias'].view(pop_size) + + inputs = torch.stack([a, b], dim=-1) # [256, pop_size, 2] + h_or = heaviside((inputs * w_or).sum(-1) + b_or) + h_nand = heaviside((inputs * w_nand).sum(-1) + b_nand) + + # Layer 2 + hidden = torch.stack([h_or, h_nand], dim=-1) + w2 = pop[f'{xor_prefix}.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{xor_prefix}.layer2.bias'].view(pop_size) + return heaviside((hidden * w2).sum(-1) + b2) + + def _test_parity_xor_tree(self, pop: Dict, prefix: str, debug: bool) -> Tuple[torch.Tensor, int]: + """Test parity circuit with XOR tree structure.""" + pop_size = next(iter(pop.values())).shape[0] + + test_vals = torch.arange(256, device=self.device, dtype=torch.long) + inputs = torch.stack([((test_vals >> (7 - i)) & 1).float() for i in range(8)], dim=1) + + # XOR of all bits: 1 if odd number of 1s + popcounts = inputs.sum(dim=1) + xor_result = (popcounts.long() % 2).float() + + try: + # Stage 1: 4 XORs (pairs of bits) + s1_out = [] + for i in range(4): + xor_out = self._eval_xor_tree_stage(pop, prefix, 1, i, inputs[:, i*2], inputs[:, i*2+1]) + s1_out.append(xor_out) + + # Stage 2: 2 XORs + s2_out = [] + for i in range(2): + xor_out = self._eval_xor_tree_stage(pop, prefix, 2, i, s1_out[i*2], s1_out[i*2+1]) + s2_out.append(xor_out) + + # Stage 3: 1 XOR + s3_out = self._eval_xor_tree_stage(pop, prefix, 3, 0, s2_out[0], s2_out[1]) + + # Output NOT (for parity checker - inverts the XOR result) + if f'{prefix}.output.not.weight' in pop: + w_not = pop[f'{prefix}.output.not.weight'].view(pop_size) + b_not = pop[f'{prefix}.output.not.bias'].view(pop_size) + out = heaviside(s3_out * w_not + b_not) + # Checker outputs 1 if even parity (XOR=0), so expected is inverted xor_result + expected = 1.0 - xor_result + else: + out = s3_out + expected = xor_result + + except KeyError as e: + return torch.zeros(pop_size, device=self.device), 0 + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(256): + if out[i, 0].item() != expected[i].item(): + failures.append((i, expected[i].item(), out[i, 0].item())) + + self._record(prefix, int(correct[0].item()), 256, failures[:10]) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, 256 + + def _test_error_detection(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test error detection circuits.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== ERROR DETECTION ===") + + # XOR tree parity circuits + for prefix in ['error_detection.paritychecker8bit', 'error_detection.paritygenerator8bit']: + s, t = self._test_parity_xor_tree(pop, prefix, debug) + scores += s + total += t + + return scores, total + + # ========================================================================= + # COMBINATIONAL LOGIC + # ========================================================================= + + def _test_mux2to1(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test 2-to-1 multiplexer.""" + pop_size = next(iter(pop.values())).shape[0] + prefix = 'combinational.multiplexer2to1' + + # Inputs: [a, b, sel] -> out = sel ? b : a + inputs = torch.tensor([ + [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], + [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], + ], device=self.device, dtype=torch.float32) + expected = torch.tensor([0, 0, 0, 1, 1, 0, 1, 1], device=self.device, dtype=torch.float32) + + try: + w = pop[f'{prefix}.weight'] + b = pop[f'{prefix}.bias'] + out = heaviside(inputs @ w.view(pop_size, -1).T + b.view(pop_size)) + except KeyError: + return torch.zeros(pop_size, device=self.device), 0 + + correct = (out == expected.unsqueeze(1)).float().sum(0) + + failures = [] + if pop_size == 1: + for i in range(8): + if out[i, 0].item() != expected[i].item(): + failures.append((inputs[i].tolist(), expected[i].item(), out[i, 0].item())) + + self._record(prefix, int(correct[0].item()), 8, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return correct, 8 + + def _test_decoder3to8(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test 3-to-8 decoder.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== DECODER 3-TO-8 ===") + + inputs = torch.tensor([ + [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], + [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], + ], device=self.device, dtype=torch.float32) + + for out_idx in range(8): + prefix = f'combinational.decoder3to8.out{out_idx}' + expected = torch.zeros(8, device=self.device) + expected[out_idx] = 1.0 + + try: + w = pop[f'{prefix}.weight'] + b = pop[f'{prefix}.bias'] + out = heaviside(inputs @ w.view(pop_size, -1).T + b.view(pop_size)) + except KeyError: + continue + + correct = (out == expected.unsqueeze(1)).float().sum(0) + scores += correct + total += 8 + + failures = [] + if pop_size == 1: + for i in range(8): + if out[i, 0].item() != expected[i].item(): + failures.append((inputs[i].tolist(), expected[i].item(), out[i, 0].item())) + + self._record(prefix, int(correct[0].item()), 8, failures) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return scores, total + + def _test_combinational(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test combinational logic circuits.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== COMBINATIONAL LOGIC ===") + + s, t = self._test_mux2to1(pop, debug) + scores += s + total += t + + s, t = self._test_decoder3to8(pop, debug) + scores += s + total += t + + s, t = self._test_barrel_shifter(pop, debug) + scores += s + total += t + + s, t = self._test_priority_encoder(pop, debug) + scores += s + total += t + + return scores, total + + def _test_barrel_shifter(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test barrel shifter (shift by 0-7 positions).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== BARREL SHIFTER ===") + + try: + # Test all shift amounts 0-7 with various input patterns + test_vals = [0b10000001, 0b11110000, 0b00001111, 0b10101010, 0xFF] + + for val in test_vals: + for shift in range(8): + expected_val = (val << shift) & 0xFF # Left shift + shift_bits = [float((shift >> (2 - i)) & 1) for i in range(3)] + + # Process through 3 layers; every intermediate stays + # per-slot ([pop_size]) so population members are + # evaluated independently. + layer_in = [torch.full((pop_size,), float((val >> (7 - i)) & 1), + device=self.device) for i in range(8)] + for layer in range(3): + shift_amount = 1 << (2 - layer) # 4, 2, 1 + sel = torch.full((pop_size,), shift_bits[layer], device=self.device) + layer_out = [] + + for bit in range(8): + prefix = f'combinational.barrelshifter.layer{layer}.bit{bit}' + + # NOT sel + w_not = pop[f'{prefix}.not_sel.weight'].view(pop_size) + b_not = pop[f'{prefix}.not_sel.bias'].view(pop_size) + not_sel = heaviside(sel * w_not + b_not) + + # Source for shifted value + shifted_src = bit + shift_amount + if shifted_src < 8: + shifted_val = layer_in[shifted_src] + else: + shifted_val = torch.zeros(pop_size, device=self.device) + + # AND a: original AND NOT sel + w_and_a = pop[f'{prefix}.and_a.weight'].view(pop_size, 2) + b_and_a = pop[f'{prefix}.and_a.bias'].view(pop_size) + inp_a = torch.stack([layer_in[bit], not_sel], dim=-1) + and_a = heaviside((inp_a * w_and_a).sum(-1) + b_and_a) + + # AND b: shifted AND sel + w_and_b = pop[f'{prefix}.and_b.weight'].view(pop_size, 2) + b_and_b = pop[f'{prefix}.and_b.bias'].view(pop_size) + inp_b = torch.stack([shifted_val, sel], dim=-1) + and_b = heaviside((inp_b * w_and_b).sum(-1) + b_and_b) + + # OR + w_or = pop[f'{prefix}.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.or.bias'].view(pop_size) + inp_or = torch.stack([and_a, and_b], dim=-1) + layer_out.append(heaviside((inp_or * w_or).sum(-1) + b_or)) + + layer_in = layer_out + + # Check result per slot + result = torch.zeros(pop_size, device=self.device) + for i in range(8): + result += layer_in[i] * (1 << (7 - i)) + scores += (result == expected_val).float() + total += 1 + + self._record('combinational.barrelshifter', int(scores[0].item()), total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" combinational.barrelshifter: SKIP ({e})") + + return scores, total + + def _test_priority_encoder(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test priority encoder (find highest set bit).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== PRIORITY ENCODER ===") + + try: + # Test cases: input -> (valid, index of highest bit) + test_cases = [ + (0b00000000, 0, 0), # No bits set, valid=0 + (0b00000001, 1, 7), # Bit 7 (LSB) + (0b00000010, 1, 6), + (0b00000100, 1, 5), + (0b00001000, 1, 4), + (0b00010000, 1, 3), + (0b00100000, 1, 2), + (0b01000000, 1, 1), + (0b10000000, 1, 0), # Bit 0 (MSB) + (0b10000001, 1, 0), # Multiple bits, highest wins + (0b01010101, 1, 1), + (0b00001111, 1, 4), + (0b11111111, 1, 0), + ] + + for val, expected_valid, expected_idx in test_cases: + val_bits = torch.tensor([float((val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # Valid output: OR of all input bits + w_valid = pop['combinational.priorityencoder.valid.weight'].view(pop_size, 8) + b_valid = pop['combinational.priorityencoder.valid.bias'].view(pop_size) + out_valid = heaviside((val_bits * w_valid).sum(-1) + b_valid) + + scores += (out_valid == float(expected_valid)).float() + total += 1 + + # Index outputs (3 bits) + if expected_valid == 1: + for idx_bit in range(3): + try: + w_idx = pop[f'combinational.priorityencoder.idx{idx_bit}.weight'].view(pop_size, 8) + b_idx = pop[f'combinational.priorityencoder.idx{idx_bit}.bias'].view(pop_size) + out_idx = heaviside((val_bits * w_idx).sum(-1) + b_idx) + expected_bit = (expected_idx >> (2 - idx_bit)) & 1 + scores += (out_idx == float(expected_bit)).float() + total += 1 + except KeyError: + pass + + self._record('combinational.priorityencoder', int(scores[0].item()), total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" combinational.priorityencoder: SKIP ({e})") + + return scores, total + + def _test_barrel_shifter_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit barrel shifter (shift by 0 to bits-1 positions).""" + import math + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + num_layers = max(1, math.ceil(math.log2(bits))) + max_val = (1 << bits) - 1 + + if debug: + print(f"\n=== {bits}-BIT BARREL SHIFTER ===") + + prefix = f'combinational.barrelshifter{bits}' + try: + if bits == 16: + test_vals = [0x8001, 0xFF00, 0x00FF, 0xAAAA, 0xFFFF, 0x1234] + elif bits == 32: + test_vals = [0x80000001, 0xFFFF0000, 0x0000FFFF, 0xAAAAAAAA, 0xFFFFFFFF, 0x12345678] + else: + test_vals = [0b10000001, 0b11110000, 0b00001111, 0b10101010, max_val] + + num_shifts = min(bits, 8) + for val in test_vals: + for shift in range(num_shifts): + expected_val = (val << shift) & max_val + shift_bits = [float((shift >> (num_layers - 1 - i)) & 1) for i in range(num_layers)] + + layer_in = [torch.full((pop_size,), float((val >> (bits - 1 - i)) & 1), + device=self.device) for i in range(bits)] + for layer in range(num_layers): + shift_amount = 1 << (num_layers - 1 - layer) + sel = torch.full((pop_size,), shift_bits[layer], device=self.device) + layer_out = [] + + for bit in range(bits): + bit_prefix = f'{prefix}.layer{layer}.bit{bit}' + + w_not = pop[f'{bit_prefix}.not_sel.weight'].view(pop_size) + b_not = pop[f'{bit_prefix}.not_sel.bias'].view(pop_size) + not_sel = heaviside(sel * w_not + b_not) + + shifted_src = bit + shift_amount + if shifted_src < bits: + shifted_val = layer_in[shifted_src] + else: + shifted_val = torch.zeros(pop_size, device=self.device) + + w_and_a = pop[f'{bit_prefix}.and_a.weight'].view(pop_size, 2) + b_and_a = pop[f'{bit_prefix}.and_a.bias'].view(pop_size) + inp_a = torch.stack([layer_in[bit], not_sel], dim=-1) + and_a = heaviside((inp_a * w_and_a).sum(-1) + b_and_a) + + w_and_b = pop[f'{bit_prefix}.and_b.weight'].view(pop_size, 2) + b_and_b = pop[f'{bit_prefix}.and_b.bias'].view(pop_size) + inp_b = torch.stack([shifted_val, sel], dim=-1) + and_b = heaviside((inp_b * w_and_b).sum(-1) + b_and_b) + + w_or = pop[f'{bit_prefix}.or.weight'].view(pop_size, 2) + b_or = pop[f'{bit_prefix}.or.bias'].view(pop_size) + inp_or = torch.stack([and_a, and_b], dim=-1) + layer_out.append(heaviside((inp_or * w_or).sum(-1) + b_or)) + + layer_in = layer_out + + result = torch.zeros(pop_size, device=self.device, dtype=torch.float64) + for i in range(bits): + result += layer_in[i].double() * (1 << (bits - 1 - i)) + scores += (result == float(expected_val)).float() + total += 1 + + self._record(prefix, int(scores[0].item()), total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" {prefix}: SKIP ({e})") + + return scores, total + + def _test_priority_encoder_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Test N-bit priority encoder (find highest set bit). + + The priority encoder is a multi-layer circuit: + 1. any_higher{pos}: OR of bits 0 to pos-1 (all higher-priority positions) + 2. is_highest{0}: bit[0] directly (MSB is always highest if set) + 3. is_highest{pos}: bit[pos] AND NOT(any_higher{pos}) for pos > 0 + 4. out{bit}: OR of is_highest{pos} for all pos where (pos >> bit) & 1 + 5. valid: OR of all input bits + """ + import math + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + out_bits = max(1, math.ceil(math.log2(bits))) + + if debug: + print(f"\n=== {bits}-BIT PRIORITY ENCODER ===") + + prefix = f'combinational.priorityencoder{bits}' + try: + test_cases = [(0, 0, 0)] + for i in range(bits): + test_cases.append((1 << i, 1, bits - 1 - i)) + if bits == 16: + test_cases.extend([ + (0x8001, 1, 0), (0x5555, 1, 1), (0x00FF, 1, 8), (0xFFFF, 1, 0) + ]) + elif bits == 32: + test_cases.extend([ + (0x80000001, 1, 0), (0x55555555, 1, 1), (0x0000FFFF, 1, 16), (0xFFFFFFFF, 1, 0) + ]) + + for val, expected_valid, expected_idx in test_cases: + val_bits = torch.tensor([float((val >> (bits - 1 - i)) & 1) for i in range(bits)], + device=self.device, dtype=torch.float32) + + w_valid = pop[f'{prefix}.valid.weight'].view(pop_size, bits) + b_valid = pop[f'{prefix}.valid.bias'].view(pop_size) + out_valid = heaviside((val_bits * w_valid).sum(-1) + b_valid) + + scores += (out_valid == float(expected_valid)).float() + total += 1 + + if expected_valid == 1: + any_higher = [None] + for pos in range(1, bits): + w = pop[f'{prefix}.any_higher{pos}.weight'].view(pop_size, -1) + b = pop[f'{prefix}.any_higher{pos}.bias'].view(pop_size) + inp = val_bits[:pos] + out = heaviside((inp * w[:, :len(inp)]).sum(-1) + b) + any_higher.append(out) + + is_highest = [] + for pos in range(bits): + if pos == 0: + is_high = val_bits[0].unsqueeze(0).expand(pop_size) + else: + w_not = pop[f'{prefix}.is_highest{pos}.not_higher.weight'].view(pop_size) + b_not = pop[f'{prefix}.is_highest{pos}.not_higher.bias'].view(pop_size) + not_higher = heaviside(any_higher[pos] * w_not + b_not) + + w_and = pop[f'{prefix}.is_highest{pos}.and.weight'].view(pop_size, -1) + b_and = pop[f'{prefix}.is_highest{pos}.and.bias'].view(pop_size) + inp = torch.stack([val_bits[pos].expand(pop_size), not_higher], dim=-1) + is_high = heaviside((inp * w_and).sum(-1) + b_and) + is_highest.append(is_high) + + for idx_bit in range(out_bits): + try: + w_idx = pop[f'{prefix}.out{idx_bit}.weight'].view(pop_size, -1) + b_idx = pop[f'{prefix}.out{idx_bit}.bias'].view(pop_size) + relevant = [is_highest[pos] for pos in range(bits) if (pos >> idx_bit) & 1] + if len(relevant) > 0: + inp = torch.stack(relevant[:w_idx.shape[1]], dim=-1) + out_idx = heaviside((inp * w_idx).sum(-1) + b_idx) + expected_bit = (expected_idx >> idx_bit) & 1 + scores += (out_idx == float(expected_bit)).float() + total += 1 + except KeyError: + pass + + self._record(prefix, int(scores[0].item()), total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" {prefix}: SKIP ({e})") + + return scores, total + + # ========================================================================= + # CONTROL FLOW + # ========================================================================= + + def _test_conditional_jump(self, pop: Dict, name: str, debug: bool) -> Tuple[torch.Tensor, int]: + """Test conditional jump circuit (N-bit address aware).""" + pop_size = next(iter(pop.values())).shape[0] + prefix = f'control.{name}' + + # Test cases: [pc_bit, target_bit, flag] -> out = flag ? target : pc + inputs = torch.tensor([ + [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], + [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], + ], device=self.device, dtype=torch.float32) + expected = torch.tensor([0, 0, 0, 1, 1, 0, 1, 1], device=self.device, dtype=torch.float32) + + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + pc = inputs[:, 0].unsqueeze(1).expand(-1, pop_size) # [8, pop] + target = inputs[:, 1].unsqueeze(1).expand(-1, pop_size) + flag = inputs[:, 2].unsqueeze(1).expand(-1, pop_size) + + for bit in range(self.addr_bits): + bit_prefix = f'{prefix}.bit{bit}' + try: + # NOT sel + w_not = pop[f'{bit_prefix}.not_sel.weight'].view(pop_size) + b_not = pop[f'{bit_prefix}.not_sel.bias'].view(pop_size) + not_sel = heaviside(flag * w_not + b_not) + + # AND a (pc AND NOT sel) + w_and_a = pop[f'{bit_prefix}.and_a.weight'].view(pop_size, 2) + b_and_a = pop[f'{bit_prefix}.and_a.bias'].view(pop_size) + inp_a = torch.stack([pc, not_sel], dim=-1) + and_a = heaviside((inp_a * w_and_a).sum(-1) + b_and_a) + + # AND b (target AND sel) + w_and_b = pop[f'{bit_prefix}.and_b.weight'].view(pop_size, 2) + b_and_b = pop[f'{bit_prefix}.and_b.bias'].view(pop_size) + inp_b = torch.stack([target, flag], dim=-1) + and_b = heaviside((inp_b * w_and_b).sum(-1) + b_and_b) + + # OR + w_or = pop[f'{bit_prefix}.or.weight'].view(pop_size, 2) + b_or = pop[f'{bit_prefix}.or.bias'].view(pop_size) + ab = torch.stack([and_a, and_b], dim=-1) # [8, pop_size, 2] + out = heaviside((ab * w_or).sum(-1) + b_or) # [8, pop_size] + + correct = (out == expected.unsqueeze(1)).float().sum(0) # [pop_size] + scores += correct + total += 8 + + except KeyError: + pass + + if total > 0: + self._record(prefix, int(scores[0].item()), total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return scores, total + + def _test_control_flow(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test control flow circuits.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== CONTROL FLOW ===") + + jumps = ['jz', 'jnz', 'jc', 'jnc', 'jn', 'jp', 'jv', 'jnv', 'conditionaljump'] + for name in jumps: + s, t = self._test_conditional_jump(pop, name, debug) + scores += s + total += t + + # Stack operations + s, t = self._test_stack_ops(pop, debug) + scores += s + total += t + + return scores, total + + def _test_stack_ops(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test PUSH/POP/RET stack operation circuits (N-bit address aware).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + addr_bits = self.addr_bits + addr_mask = (1 << addr_bits) - 1 + + if debug: + print(f"\n=== STACK OPERATIONS ({addr_bits}-bit SP) ===") + + # Test PUSH SP decrement (addr_bits wide, borrow chain) + try: + # Generate test values appropriate for addr_bits + sp_tests = [0, 1, addr_mask // 2, addr_mask] + if addr_bits >= 8: + sp_tests.append(0x100 & addr_mask) + if addr_bits >= 12: + sp_tests.append(0x1234 & addr_mask) + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for sp_val in sp_tests: + expected_val = (sp_val - 1) & addr_mask + sp_bits = [float((sp_val >> (addr_bits - 1 - i)) & 1) for i in range(addr_bits)] + + borrow = torch.ones(pop_size, device=self.device) + out_bits = [] + for bit in range(addr_bits - 1, -1, -1): # LSB to MSB + prefix = f'control.push.sp_dec.bit{bit}' + + w_or = pop[f'{prefix}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{prefix}.xor.layer1.nand.bias'].view(pop_size) + w2 = pop[f'{prefix}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{prefix}.xor.layer2.bias'].view(pop_size) + + sp_bit = torch.full((pop_size,), sp_bits[bit], device=self.device) + inp = torch.stack([sp_bit, borrow], dim=-1) + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + hidden = torch.stack([h_or, h_nand], dim=-1) + diff_bit = heaviside((hidden * w2).sum(-1) + b2) + out_bits.insert(0, diff_bit) + + # Borrow: NOT(sp) AND borrow_in + not_sp = torch.full((pop_size,), 1.0 - sp_bits[bit], device=self.device) + w_borrow = pop[f'{prefix}.borrow.weight'].view(pop_size, 2) + b_borrow = pop[f'{prefix}.borrow.bias'].view(pop_size) + borrow_inp = torch.stack([not_sp, borrow], dim=-1) + borrow = heaviside((borrow_inp * w_borrow).sum(-1) + b_borrow) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (addr_bits - 1 - i)) & 1) for i in range(addr_bits)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += addr_bits + + scores += op_scores + total += op_total + self._record('control.push.sp_dec', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" control.push.sp_dec: SKIP ({e})") + + # Test POP SP increment (addr_bits wide, carry chain) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for sp_val in sp_tests: + expected_val = (sp_val + 1) & addr_mask + sp_bits = [float((sp_val >> (addr_bits - 1 - i)) & 1) for i in range(addr_bits)] + + carry = torch.ones(pop_size, device=self.device) + out_bits = [] + for bit in range(addr_bits - 1, -1, -1): # LSB to MSB + prefix = f'control.pop.sp_inc.bit{bit}' + + w_or = pop[f'{prefix}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'{prefix}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'{prefix}.xor.layer1.nand.bias'].view(pop_size) + w2 = pop[f'{prefix}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'{prefix}.xor.layer2.bias'].view(pop_size) + + sp_bit = torch.full((pop_size,), sp_bits[bit], device=self.device) + inp = torch.stack([sp_bit, carry], dim=-1) + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + hidden = torch.stack([h_or, h_nand], dim=-1) + sum_bit = heaviside((hidden * w2).sum(-1) + b2) + out_bits.insert(0, sum_bit) + + # Carry: sp AND carry_in + w_carry = pop[f'{prefix}.carry.weight'].view(pop_size, 2) + b_carry = pop[f'{prefix}.carry.bias'].view(pop_size) + carry = heaviside((inp * w_carry).sum(-1) + b_carry) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (addr_bits - 1 - i)) & 1) for i in range(addr_bits)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += addr_bits + + scores += op_scores + total += op_total + self._record('control.pop.sp_inc', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" control.pop.sp_inc: SKIP ({e})") + + # Test RET address buffer (addr_bits identity gates) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + ret_tests = [0, addr_mask, addr_mask // 2, 1] + if addr_bits >= 12: + ret_tests.append(0x1234 & addr_mask) + for addr_val in ret_tests: + ret_bits_tensor = torch.tensor([float((addr_val >> (addr_bits - 1 - i)) & 1) for i in range(addr_bits)], + device=self.device, dtype=torch.float32) + + out_bits = [] + for bit in range(addr_bits): + w = pop[f'control.ret.addr.bit{bit}.weight'].view(pop_size) + b = pop[f'control.ret.addr.bit{bit}.bias'].view(pop_size) + out = heaviside(ret_bits_tensor[bit] * w + b) + out_bits.append(out) + + out = torch.stack(out_bits, dim=-1) + correct = (out == ret_bits_tensor.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += addr_bits + + scores += op_scores + total += op_total + self._record('control.ret.addr', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" control.ret.addr: SKIP ({e})") + + return scores, total + + # ========================================================================= + # ALU + # ========================================================================= + + def _test_alu_ops(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test ALU operations (8-bit bitwise).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== ALU OPERATIONS ===") + + # Test ALU AND/OR/NOT on 8-bit values + # Each ALU op has weight [16] or [8] and bias [8] + # Structured as 8 parallel 2-input (or 1-input for NOT) gates + + test_vals = [(0, 0), (255, 255), (0xAA, 0x55), (0x0F, 0xF0)] + + # AND: weight [16] = 8 * [2], bias [8] + try: + w = pop['alu.alu8bit.and.weight'].view(pop_size, 8, 2) # [pop, 8, 2] + b = pop['alu.alu8bit.and.bias'].view(pop_size, 8) # [pop, 8] + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for a_val, b_val in test_vals: + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + b_bits = torch.tensor([((b_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + # [8, 2] + inputs = torch.stack([a_bits, b_bits], dim=-1) + # [pop, 8] + out = heaviside((inputs * w).sum(-1) + b) + expected = torch.tensor([((a_val & b_val) >> (7 - i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) # [pop] + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.and', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError): + pass + + # OR + try: + w = pop['alu.alu8bit.or.weight'].view(pop_size, 8, 2) + b = pop['alu.alu8bit.or.bias'].view(pop_size, 8) + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for a_val, b_val in test_vals: + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + b_bits = torch.tensor([((b_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + inputs = torch.stack([a_bits, b_bits], dim=-1) + out = heaviside((inputs * w).sum(-1) + b) + expected = torch.tensor([((a_val | b_val) >> (7 - i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.or', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError): + pass + + # NOT + try: + w = pop['alu.alu8bit.not.weight'].view(pop_size, 8) + b = pop['alu.alu8bit.not.bias'].view(pop_size, 8) + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for a_val, _ in test_vals: + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + out = heaviside(a_bits * w + b) + expected = torch.tensor([(((~a_val) & 0xFF) >> (7 - i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.not', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError): + pass + + # SHL (shift left) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for a_val, _ in test_vals: + expected_val = (a_val << 1) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + out_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.shl.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu8bit.shl.bit{bit}.bias'].view(pop_size) + if bit < 7: + inp = a_bits[bit + 1].unsqueeze(0).expand(pop_size) + else: + inp = torch.zeros(pop_size, device=self.device) + out = heaviside(inp * w + b) + out_bits.append(out) + out = torch.stack(out_bits, dim=-1) # [pop, 8] + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.shl', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.shl: SKIP ({e})") + + # SHR (shift right) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + for a_val, _ in test_vals: + expected_val = (a_val >> 1) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + out_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.shr.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu8bit.shr.bit{bit}.bias'].view(pop_size) + if bit > 0: + inp = a_bits[bit - 1].unsqueeze(0).expand(pop_size) + else: + inp = torch.zeros(pop_size, device=self.device) + out = heaviside(inp * w + b) + out_bits.append(out) + out = torch.stack(out_bits, dim=-1) # [pop, 8] + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.shr', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.shr: SKIP ({e})") + + # MUL (partial products only - just verify AND gates work) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + mul_tests = [(3, 4), (7, 8), (15, 17), (0, 255)] + for a_val, b_val in mul_tests: + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + b_bits = torch.tensor([((b_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # Test partial product AND gates + for i in range(8): + for j in range(8): + w = pop[f'alu.alu8bit.mul.pp.a{i}b{j}.weight'].view(pop_size, 2) + b = pop[f'alu.alu8bit.mul.pp.a{i}b{j}.bias'].view(pop_size) + inp = torch.tensor([a_bits[i].item(), b_bits[j].item()], device=self.device) + out = heaviside((inp * w).sum(-1) + b) + expected = float(int(a_bits[i].item()) & int(b_bits[j].item())) + correct = (out == expected).float() + op_scores += correct + op_total += 1 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.mul', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.mul: SKIP ({e})") + + # DIV: drive each stage's bit-cascade GE comparator along the real + # restoring-division remainder trace for each operand pair. + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + div_tests = [(100, 10), (255, 17), (50, 7), (128, 16), (255, 255), + (254, 255), (7, 130), (200, 3), (9, 3), (1, 1)] + # Remainder value and expected GE at each stage, per pair. + stage_rems = [[] for _ in range(8)] + stage_ges = [[] for _ in range(8)] + for a_val, b_val in div_tests: + a_bits_int = [(a_val >> (7 - i)) & 1 for i in range(8)] + remainder = 0 + for stage in range(8): + remainder = ((remainder << 1) | a_bits_int[stage]) & 0xFF + stage_rems[stage].append(remainder) + ge = 1.0 if remainder >= b_val else 0.0 + stage_ges[stage].append(ge) + if ge: + remainder -= b_val + + div_vals = torch.tensor([b for _, b in div_tests], device=self.device, dtype=torch.long) + div_bits = torch.stack([((div_vals >> (7 - i)) & 1).float() for i in range(8)], dim=1) + for stage in range(8): + rem_vals = torch.tensor(stage_rems[stage], device=self.device, dtype=torch.long) + rem_bits = torch.stack([((rem_vals >> (7 - i)) & 1).float() for i in range(8)], dim=1) + outs = self._eval_bit_cascade_compare( + pop, + f'alu.alu8bit.div.stage{stage}.cmp_bc', + f'alu.alu8bit.div.stage{stage}.cmp_bc.gt', + f'alu.alu8bit.div.stage{stage}.cmp_bc.lt', + f'alu.alu8bit.div.stage{stage}.cmp', + f'alu.alu8bit.div.stage{stage}.cmp_bc.le', + f'alu.alu8bit.div.stage{stage}.cmp_bc.eq', + 8, rem_bits, div_bits, + ) + expected = torch.tensor(stage_ges[stage], device=self.device) + correct = (outs['ge'] == expected.unsqueeze(1)).float().sum(0) # [pop] + op_scores += correct + op_total += len(div_tests) + + scores += op_scores + total += op_total + self._record('alu.alu8bit.div', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.div: SKIP ({e})") + + # INC (increment by 1) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + inc_tests = [0, 1, 127, 128, 254, 255] + for a_val in inc_tests: + expected_val = (a_val + 1) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # INC uses half-adder chain with initial carry = 1 + carry = torch.ones(pop_size, device=self.device) + out_bits = [] + for bit in range(7, -1, -1): # LSB to MSB + # XOR for sum + w_or = pop[f'alu.alu8bit.inc.bit{bit}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'alu.alu8bit.inc.bit{bit}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'alu.alu8bit.inc.bit{bit}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'alu.alu8bit.inc.bit{bit}.xor.layer1.nand.bias'].view(pop_size) + w2 = pop[f'alu.alu8bit.inc.bit{bit}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'alu.alu8bit.inc.bit{bit}.xor.layer2.bias'].view(pop_size) + + inp = torch.stack([a_bits[bit].expand(pop_size), carry], dim=-1) + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + hidden = torch.stack([h_or, h_nand], dim=-1) + sum_bit = heaviside((hidden * w2).sum(-1) + b2) + out_bits.insert(0, sum_bit) + + # AND for carry + w_carry = pop[f'alu.alu8bit.inc.bit{bit}.carry.weight'].view(pop_size, 2) + b_carry = pop[f'alu.alu8bit.inc.bit{bit}.carry.bias'].view(pop_size) + carry = heaviside((inp * w_carry).sum(-1) + b_carry) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.inc', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.inc: SKIP ({e})") + + # DEC (decrement by 1) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + dec_tests = [0, 1, 127, 128, 254, 255] + for a_val in dec_tests: + expected_val = (a_val - 1) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # DEC uses borrow chain + borrow = torch.ones(pop_size, device=self.device) + out_bits = [] + for bit in range(7, -1, -1): + w_or = pop[f'alu.alu8bit.dec.bit{bit}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'alu.alu8bit.dec.bit{bit}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'alu.alu8bit.dec.bit{bit}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'alu.alu8bit.dec.bit{bit}.xor.layer1.nand.bias'].view(pop_size) + w2 = pop[f'alu.alu8bit.dec.bit{bit}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'alu.alu8bit.dec.bit{bit}.xor.layer2.bias'].view(pop_size) + + inp = torch.stack([a_bits[bit].expand(pop_size), borrow], dim=-1) + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + hidden = torch.stack([h_or, h_nand], dim=-1) + diff_bit = heaviside((hidden * w2).sum(-1) + b2) + out_bits.insert(0, diff_bit) + + # Borrow logic: borrow_out = NOT(a) AND borrow_in + w_not = pop[f'alu.alu8bit.dec.bit{bit}.not_a.weight'].view(pop_size) + b_not = pop[f'alu.alu8bit.dec.bit{bit}.not_a.bias'].view(pop_size) + not_a = heaviside(a_bits[bit] * w_not + b_not) + + w_borrow = pop[f'alu.alu8bit.dec.bit{bit}.borrow.weight'].view(pop_size, 2) + b_borrow = pop[f'alu.alu8bit.dec.bit{bit}.borrow.bias'].view(pop_size) + borrow_inp = torch.stack([not_a, borrow], dim=-1) + borrow = heaviside((borrow_inp * w_borrow).sum(-1) + b_borrow) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.dec', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.dec: SKIP ({e})") + + # NEG (two's complement: NOT + 1) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + neg_tests = [0, 1, 127, 128, 255] + for a_val in neg_tests: + expected_val = (-a_val) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # First NOT each bit + not_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.neg.not.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu8bit.neg.not.bit{bit}.bias'].view(pop_size) + not_bit = heaviside(a_bits[bit] * w + b) + not_bits.append(not_bit) + + # Then INC + carry = torch.ones(pop_size, device=self.device) + out_bits = [] + for bit in range(7, -1, -1): + w_or = pop[f'alu.alu8bit.neg.inc.bit{bit}.xor.layer1.or.weight'].view(pop_size, 2) + b_or = pop[f'alu.alu8bit.neg.inc.bit{bit}.xor.layer1.or.bias'].view(pop_size) + w_nand = pop[f'alu.alu8bit.neg.inc.bit{bit}.xor.layer1.nand.weight'].view(pop_size, 2) + b_nand = pop[f'alu.alu8bit.neg.inc.bit{bit}.xor.layer1.nand.bias'].view(pop_size) + w2 = pop[f'alu.alu8bit.neg.inc.bit{bit}.xor.layer2.weight'].view(pop_size, 2) + b2 = pop[f'alu.alu8bit.neg.inc.bit{bit}.xor.layer2.bias'].view(pop_size) + + inp = torch.stack([not_bits[bit], carry], dim=-1) + h_or = heaviside((inp * w_or).sum(-1) + b_or) + h_nand = heaviside((inp * w_nand).sum(-1) + b_nand) + hidden = torch.stack([h_or, h_nand], dim=-1) + sum_bit = heaviside((hidden * w2).sum(-1) + b2) + out_bits.insert(0, sum_bit) + + w_carry = pop[f'alu.alu8bit.neg.inc.bit{bit}.carry.weight'].view(pop_size, 2) + b_carry = pop[f'alu.alu8bit.neg.inc.bit{bit}.carry.bias'].view(pop_size) + carry = heaviside((inp * w_carry).sum(-1) + b_carry) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.neg', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.neg: SKIP ({e})") + + # ROL (rotate left - MSB wraps to LSB) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + rol_tests = [0b10000000, 0b00000001, 0b10101010, 0b01010101, 0xFF, 0x00] + for a_val in rol_tests: + expected_val = ((a_val << 1) | (a_val >> 7)) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + out_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.rol.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu8bit.rol.bit{bit}.bias'].view(pop_size) + # ROL: bit[i] gets bit[i+1], bit[7] gets bit[0] + src_bit = (bit + 1) % 8 + out = heaviside(a_bits[src_bit] * w + b) + out_bits.append(out) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.rol', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.rol: SKIP ({e})") + + # ROR (rotate right - LSB wraps to MSB) + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + ror_tests = [0b10000000, 0b00000001, 0b10101010, 0b01010101, 0xFF, 0x00] + for a_val in ror_tests: + expected_val = ((a_val >> 1) | (a_val << 7)) & 0xFF + a_bits = torch.tensor([((a_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + out_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.ror.bit{bit}.weight'].view(pop_size) + b = pop[f'alu.alu8bit.ror.bit{bit}.bias'].view(pop_size) + # ROR: bit[i] gets bit[i-1], bit[0] gets bit[7] + src_bit = (bit - 1) % 8 + out = heaviside(a_bits[src_bit] * w + b) + out_bits.append(out) + + out = torch.stack(out_bits, dim=-1) + expected = torch.tensor([((expected_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + correct = (out == expected.unsqueeze(0)).float().sum(1) + op_scores += correct + op_total += 8 + + scores += op_scores + total += op_total + self._record('alu.alu8bit.ror', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" alu.alu8bit.ror: SKIP ({e})") + + return scores, total + + # ========================================================================= + # MANIFEST + # ========================================================================= + + def _test_manifest(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Verify manifest values.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== MANIFEST ===") + + fixed_expected = { + 'manifest.alu_operations': 16.0, + 'manifest.flags': 4.0, + 'manifest.instruction_width': 16.0, + 'manifest.register_width': 8.0, + 'manifest.registers': 4.0, + 'manifest.version': 4.0, + } + + for name, exp_val in fixed_expected.items(): + try: + val = pop[name][0, 0].item() + if val == exp_val: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(exp_val, val)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + pass + + variable_checks = ['manifest.memory_bytes', 'manifest.pc_width', 'manifest.turing_complete'] + for name in variable_checks: + try: + val = pop[name][0, 0].item() + valid = val >= 0 + if valid: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [('>=0', val)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'} (value={val})") + except KeyError: + pass + + return scores, total + + # ========================================================================= + # MEMORY + # ========================================================================= + + def _test_memory(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test memory circuits (shape validation).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== MEMORY ===") + + try: + mem_bytes = int(pop['manifest.memory_bytes'][0].item()) + addr_bits = int(pop['manifest.pc_width'][0].item()) + except KeyError: + mem_bytes = 65536 + addr_bits = 16 + + if mem_bytes == 0: + if debug: + print(" No memory (pure ALU mode)") + return scores, 0 + + expected_shapes = { + 'memory.addr_decode.weight': (mem_bytes, addr_bits), + 'memory.addr_decode.bias': (mem_bytes,), + 'memory.read.and.weight': (8, mem_bytes, 2), + 'memory.read.and.bias': (8, mem_bytes), + 'memory.read.or.weight': (8, mem_bytes), + 'memory.read.or.bias': (8,), + 'memory.write.sel.weight': (mem_bytes, 2), + 'memory.write.sel.bias': (mem_bytes,), + 'memory.write.nsel.weight': (mem_bytes, 1), + 'memory.write.nsel.bias': (mem_bytes,), + 'memory.write.and_old.weight': (mem_bytes, 8, 2), + 'memory.write.and_old.bias': (mem_bytes, 8), + 'memory.write.and_new.weight': (mem_bytes, 8, 2), + 'memory.write.and_new.bias': (mem_bytes, 8), + 'memory.write.or.weight': (mem_bytes, 8, 2), + 'memory.write.or.bias': (mem_bytes, 8), + } + + for name, expected_shape in expected_shapes.items(): + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) # Skip pop_size dimension + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + pass + + return scores, total + + # ========================================================================= + # FLOAT TESTS + # + # unpack/pack buffers and the classify subcircuit are functionally tested + # (inputs driven through the gates, outputs compared to IEEE 754 + # semantics). The composed add/mul/div/cmp pipelines are self-contained: + # their full wiring ships as .inputs metadata, so each is reconstructed + # with NetlistEvaluator and evaluated end to end against exact integer + # oracles (round-to-nearest-even, bit-exact to IEEE hardware). The + # remaining per-stage shape checks below just confirm the gate inventory. + # ========================================================================= + + def _test_float_unpack_pack(self, pop: Dict, family: str, word_bits: int, + debug: bool) -> Tuple[torch.Tensor, int]: + """Functionally test the unpack/pack identity buffers: every bit gate + must reproduce its binary input (0 -> 0, 1 -> 1).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {family.upper()} UNPACK/PACK (functional) ===") + + inputs = torch.tensor([[0.0], [1.0]], device=self.device) + expected = torch.tensor([0.0, 1.0], device=self.device) + for stage in ("unpack", "pack"): + try: + ok = torch.zeros(pop_size, device=self.device) + t = 0 + for i in range(word_bits): + w = pop[f'{family}.{stage}.bit{i}.weight'].view(pop_size, 1) + b = pop[f'{family}.{stage}.bit{i}.bias'].view(pop_size) + out = heaviside(inputs @ w.T + b) # [2, pop] + ok += (out == expected.unsqueeze(1)).float().sum(0) + t += 2 + except KeyError: + continue + scores += ok + total += t + self._record(f'{family}.{stage}', int(ok[0].item()), t, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + + return scores, total + + def _test_float_classify(self, pop: Dict, family: str, exp_bits: int, + frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Functionally test the classify subcircuit: the exponent/fraction + field predicates and the is_zero / is_subnormal / is_inf / is_nan AND + gates, against IEEE 754 categories over edge-case encodings.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {family.upper()} CLASSIFY (functional) ===") + + exp_max_val = (1 << exp_bits) - 1 + frac_max_val = (1 << frac_bits) - 1 + # (exponent, fraction) pairs covering every IEEE category at both extremes. + cases = [ + (0, 0), # zero + (0, 1), # smallest subnormal + (0, frac_max_val), # largest subnormal + (1, 0), # smallest normal + (exp_max_val - 1, frac_max_val), # largest normal + (exp_max_val // 2, 0), # mid-range normal + (exp_max_val, 0), # infinity + (exp_max_val, 1), # NaN, minimal payload + (exp_max_val, frac_max_val), # NaN, full payload + ] + num = len(cases) + exp_vals = torch.tensor([c[0] for c in cases], device=self.device, dtype=torch.long) + frac_vals = torch.tensor([c[1] for c in cases], device=self.device, dtype=torch.long) + exp_in = torch.stack([((exp_vals >> (exp_bits - 1 - i)) & 1).float() + for i in range(exp_bits)], dim=1) + frac_in = torch.stack([((frac_vals >> (frac_bits - 1 - i)) & 1).float() + for i in range(frac_bits)], dim=1) + + try: + def gate(name, inp): + w = pop[f'{name}.weight'].view(pop_size, -1) + b = pop[f'{name}.bias'].view(pop_size) + return heaviside(inp @ w.T + b) # [num, pop] + + def and_gate(name, x, y): + w = pop[f'{name}.weight'].view(pop_size, 2) + b = pop[f'{name}.bias'].view(pop_size) + inp = torch.stack([x, y], dim=-1) + return heaviside((inp * w).sum(-1) + b) + + exp_zero = gate(f'{family}.classify.exp_zero', exp_in) + exp_maxg = gate(f'{family}.classify.exp_max', exp_in) + frac_zero = gate(f'{family}.classify.frac_zero', frac_in) + frac_nz = gate(f'{family}.classify.frac_nonzero', frac_in) + + checks = [ + (f'{family}.classify.exp_zero', exp_zero, + [e == 0 for e, _ in cases]), + (f'{family}.classify.exp_max', exp_maxg, + [e == exp_max_val for e, _ in cases]), + (f'{family}.classify.frac_zero', frac_zero, + [f == 0 for _, f in cases]), + (f'{family}.classify.frac_nonzero', frac_nz, + [f != 0 for _, f in cases]), + (f'{family}.classify.is_zero.and', + and_gate(f'{family}.classify.is_zero.and', exp_zero, frac_zero), + [e == 0 and f == 0 for e, f in cases]), + (f'{family}.classify.is_subnormal.and', + and_gate(f'{family}.classify.is_subnormal.and', exp_zero, frac_nz), + [e == 0 and f != 0 for e, f in cases]), + (f'{family}.classify.is_inf.and', + and_gate(f'{family}.classify.is_inf.and', exp_maxg, frac_zero), + [e == exp_max_val and f == 0 for e, f in cases]), + (f'{family}.classify.is_nan.and', + and_gate(f'{family}.classify.is_nan.and', exp_maxg, frac_nz), + [e == exp_max_val and f != 0 for e, f in cases]), + ] + for name, out, exp_list in checks: + expected = torch.tensor([1.0 if x else 0.0 for x in exp_list], + device=self.device) + correct = (out == expected.unsqueeze(1)).float().sum(0) + scores += correct + total += num + self._record(name, int(correct[0].item()), num, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError as e: + if debug: + print(f" {family}.classify: SKIP (missing {e})") + + return scores, total + + def _test_float_cmp_composed(self, pop: Dict, family: str, exp_bits: int, + frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]: + """Composed IEEE comparison test driven entirely by the shipped + wiring: the netlist is reconstructed from the .inputs metadata and + evaluated end to end, then checked against exact IEEE semantics + (NaN unordered, +0 == -0, subnormals ordered, mixed signs).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {family.upper()} CMP (composed, from .inputs netlist) ===") + + prefix = f"{family}.cmp" + try: + ne = NetlistEvaluator(pop, self.signal_registry, prefix, pop_size=pop_size) + except (KeyError, ValueError) as e: + if debug: + print(f" {prefix} composed: SKIP ({e})") + return scores, 0 + if f"{prefix}.same_sign" not in ne.gates: + if debug: + print(f" {prefix} composed: SKIP (pre-composition wiring)") + return scores, 0 + + directed, randoms = float_test_words(exp_bits, frac_bits) + pairs = [(x, y) for x in directed for y in directed] + pairs += list(zip(randoms, randoms[1:])) + pairs += [(r, r) for r in randoms[:8]] + + W = 1 + exp_bits + frac_bits + a_words = torch.tensor([p[0] for p in pairs], dtype=torch.long) + b_words = torch.tensor([p[1] for p in pairs], dtype=torch.long) + ext = {} + for i in range(W): + ext[f"$a[{i}]"] = ((a_words >> (W - 1 - i)) & 1).float() + ext[f"$b[{i}]"] = ((b_words >> (W - 1 - i)) & 1).float() + try: + out = ne.run(ext) + except KeyError as e: + if debug: + print(f" {prefix} composed: SKIP (unbound signal {e})") + return scores, 0 + + av = [float_bits_to_value(p[0], exp_bits, frac_bits) for p in pairs] + bv = [float_bits_to_value(p[1], exp_bits, frac_bits) for p in pairs] + ops = [ + ("eq", lambda x, y: x == y), + ("lt", lambda x, y: x < y), + ("gt", lambda x, y: x > y), + ("le", lambda x, y: x <= y), + ("ge", lambda x, y: x >= y), + ] + for op, fn in ops: + expected = torch.tensor([1.0 if fn(x, y) else 0.0 for x, y in zip(av, bv)], + device=self.device) + got = out[f"{prefix}.{op}.result"] + correct = (got == expected.unsqueeze(1)).float().sum(0) + scores += correct + total += len(pairs) + failures = [] + if pop_size == 1: + for i in range(len(pairs)): + if got[i, 0].item() != expected[i].item(): + failures.append((list(pairs[i]), expected[i].item(), got[i, 0].item())) + self._record(f"{prefix}.{op}.composed", int(correct[0].item()), len(pairs), failures[:10]) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + for inp, exp_v, got_v in (failures or [])[:4]: + print(f" FAIL: a={inp[0]:#x} b={inp[1]:#x} expected {exp_v}, got {got_v}") + + return scores, total + + def _test_float_arith_composed(self, pop: Dict, family: str, op: str, + oracle, exp_bits: int, frac_bits: int, + debug: bool) -> Tuple[torch.Tensor, int]: + """Composed arithmetic test for a self-contained float pipeline: the + netlist is rebuilt from the .inputs metadata, evaluated end to end + over IEEE edge-case and random operand pairs, and the assembled + output word is compared to the exact integer oracle.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print(f"\n=== {family.upper()} {op.upper()} (composed, from .inputs netlist) ===") + + prefix = f"{family}.{op}" + try: + ne = NetlistEvaluator(pop, self.signal_registry, prefix, pop_size=pop_size) + except (KeyError, ValueError) as e: + if debug: + print(f" {prefix} composed: SKIP ({e})") + return scores, 0 + if (f"{prefix}.sel.norm" not in ne.gates + and f"{prefix}.sel.dp_norm" not in ne.gates): + if debug: + print(f" {prefix} composed: SKIP (pre-composition wiring)") + return scores, 0 + + E, F = exp_bits, frac_bits + W = 1 + E + F + directed, randoms = float_test_words(E, F) + pairs = [(x, y) for x in directed for y in directed] + pairs += list(zip(randoms, randoms[1:])) + pairs += [(r, r) for r in randoms[:8]] + + a_words = torch.tensor([p[0] for p in pairs], dtype=torch.long) + b_words = torch.tensor([p[1] for p in pairs], dtype=torch.long) + ext = {} + for i in range(W): + ext[f"$a[{i}]"] = ((a_words >> (W - 1 - i)) & 1).float() + ext[f"$b[{i}]"] = ((b_words >> (W - 1 - i)) & 1).float() + try: + out = ne.run(ext) + except KeyError as e: + if debug: + print(f" {prefix} composed: SKIP (unbound signal {e})") + return scores, 0 + + # Assemble the output word: sign, exponent (LSB-first gate index), + # fraction (LSB-first gate index). + got = out[f"{prefix}.sign_out"].double() * float(1 << (E + F)) + for k in range(E): + got = got + out[f"{prefix}.exp_out.bit{k}"].double() * float(1 << (F + k)) + for k in range(F): + got = got + out[f"{prefix}.frac_out.bit{k}"].double() * float(1 << k) + + expected = torch.tensor( + [float(oracle(p[0], p[1], E, F)) for p in pairs], dtype=torch.float64 + ).unsqueeze(1) + correct = (got == expected).float().sum(0) + scores += correct + total += len(pairs) + + failures = [] + if pop_size == 1: + for i in range(len(pairs)): + if got[i, 0].item() != expected[i, 0].item(): + failures.append((list(pairs[i]), int(expected[i, 0].item()), + int(got[i, 0].item()))) + self._record(f"{prefix}.composed", int(correct[0].item()), len(pairs), failures[:10]) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + for inp, exp_v, got_v in (failures or [])[:4]: + print(f" FAIL: a={inp[0]:#x} b={inp[1]:#x} expected {exp_v:#x}, got {got_v:#x}") + + return scores, total + + # ========================================================================= + # FLOAT16 STRUCTURE CHECKS + # ========================================================================= + + def _test_float16_core(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float16 core gate inventory.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT16 CORE (structure) ===") + + expected_gates = [ + ('float16.unpack.bit0.weight', (1,)), + ('float16.classify.exp_zero.weight', (5,)), + ('float16.classify.exp_max.weight', (5,)), + ('float16.classify.frac_zero.weight', (10,)), + ('float16.classify.is_zero.and.weight', (2,)), + ('float16.classify.is_nan.and.weight', (2,)), + ('float16.normalize.stage0.bit0.not_sel.weight', (1,)), + ('float16.normalize.stage0.bit0.and_a.weight', (2,)), + ('float16.normalize.stage0.bit0.or.weight', (2,)), + ('float16.pack.bit0.weight', (1,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float16_add(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float16 addition stage gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT16 ADD (structure) ===") + + expected_gates = [ + ('float16.add.pl.gt.weight', (15,)), # payload-cascade final OR + ('float16.add.exp_diff.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float16.add.align.s0.bit0.not_sel.weight', (1,)), + ('float16.add.sign_xor.layer1.or.weight', (2,)), + ('float16.add.addp.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float16.add.subp.not_s.bit0.weight', (1,)), + ('float16.add.res.bit0.or.weight', (2,)), + ('float16.add.lzc.nz.weight', (14,)), + ('float16.add.sel.dp_norm.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float16_mul(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float16 multiplication stage gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT16 MUL (structure) ===") + + expected_gates = [ + ('float16.mul.sign_xor.layer1.or.weight', (2,)), + ('float16.mul.exp_add.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float16.mul.exp_r.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float16.mul.mant_mul.pp.a0b0.weight', (2,)), + ('float16.mul.mant_mul.acc.s0.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float16.mul.norm.bit0.or.weight', (2,)), + ('float16.mul.sel.norm.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float16_div(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float16 division stage gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT16 DIV (structure) ===") + + expected_gates = [ + ('float16.div.sign_xor.layer1.or.weight', (2,)), + ('float16.div.exp_nb.bit0.weight', (1,)), + ('float16.div.exp_r.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float16.div.mant_div.stage0.cmp.weight', (1,)), # bit-cascaded GE = NOT(LT) buffer + ('float16.div.mant_div.stage0.q.weight', (2,)), + ('float16.div.mant_div.stage0.sub.not_d.bit0.weight', (1,)), + ('float16.div.mant_div.stage0.mux.bit0.not_sel.weight', (1,)), + ('float16.div.sel.norm.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float16_cmp(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float16 comparison gate inventory.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT16 CMP (structure) ===") + + expected_gates = [ + ('float16.cmp.a.exp_max.weight', (5,)), + ('float16.cmp.a.frac_nz.weight', (10,)), + ('float16.cmp.a.is_nan.weight', (2,)), + ('float16.cmp.either_nan.weight', (2,)), + ('float16.cmp.sign_xor.layer1.or.weight', (2,)), + ('float16.cmp.both_zero.weight', (2,)), + ('float16.cmp.mag_a_gt_b.weight', (15,)), # bit-cascaded final OR over 15 bits + ('float16.cmp.eq.result.weight', (2,)), + ('float16.cmp.lt.result.weight', (3,)), + ('float16.cmp.gt.result.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + # ========================================================================= + # FLOAT32 TESTS + # ========================================================================= + + def _test_float32_core(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float32 core gate inventory.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT32 CORE (structure) ===") + + expected_gates = [ + ('float32.unpack.bit0.weight', (1,)), + ('float32.classify.exp_zero.weight', (8,)), + ('float32.classify.exp_max.weight', (8,)), + ('float32.classify.frac_zero.weight', (23,)), + ('float32.classify.is_zero.and.weight', (2,)), + ('float32.classify.is_nan.and.weight', (2,)), + ('float32.normalize.stage0.bit0.not_sel.weight', (1,)), + ('float32.pack.bit0.weight', (1,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float32_add(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float32 addition stage gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT32 ADD (structure) ===") + + expected_gates = [ + ('float32.add.pl.gt.weight', (31,)), # payload-cascade final OR + ('float32.add.exp_diff.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float32.add.align.s0.bit0.not_sel.weight', (1,)), + ('float32.add.sign_xor.layer1.or.weight', (2,)), + ('float32.add.addp.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float32.add.subp.not_s.bit0.weight', (1,)), + ('float32.add.res.bit0.or.weight', (2,)), + ('float32.add.lzc.nz.weight', (27,)), + ('float32.add.sel.dp_norm.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float32_mul(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float32 multiplication stage gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT32 MUL (structure) ===") + + expected_gates = [ + ('float32.mul.sign_xor.layer1.or.weight', (2,)), + ('float32.mul.exp_add.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float32.mul.exp_r.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float32.mul.mant_mul.pp.a0b0.weight', (2,)), + ('float32.mul.mant_mul.acc.s0.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float32.mul.norm.bit0.or.weight', (2,)), + ('float32.mul.sel.norm.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float32_div(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float32 division stage gates.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT32 DIV (structure) ===") + + expected_gates = [ + ('float32.div.sign_xor.layer1.or.weight', (2,)), + ('float32.div.exp_nb.bit0.weight', (1,)), + ('float32.div.exp_r.fa0.ha1.sum.layer1.or.weight', (2,)), + ('float32.div.mant_div.stage0.cmp.weight', (1,)), # bit-cascaded GE = NOT(LT) buffer + ('float32.div.mant_div.stage0.q.weight', (2,)), + ('float32.div.mant_div.stage0.sub.not_d.bit0.weight', (1,)), + ('float32.div.mant_div.stage0.mux.bit0.not_sel.weight', (1,)), + ('float32.div.sel.norm.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + def _test_float32_cmp(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Structure (shape) checks for the float32 comparison gate inventory.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== FLOAT32 CMP (structure) ===") + + expected_gates = [ + ('float32.cmp.a.exp_max.weight', (8,)), + ('float32.cmp.a.frac_nz.weight', (23,)), + ('float32.cmp.a.is_nan.weight', (2,)), + ('float32.cmp.either_nan.weight', (2,)), + ('float32.cmp.sign_xor.layer1.or.weight', (2,)), + ('float32.cmp.both_zero.weight', (2,)), + ('float32.cmp.mag_a_gt_b.weight', (31,)), # bit-cascaded final OR over 31 bits + ('float32.cmp.eq.result.weight', (2,)), + ('float32.cmp.lt.result.weight', (3,)), + ('float32.cmp.gt.result.weight', (3,)), + ] + + for name, expected_shape in expected_gates: + try: + tensor = pop[name] + actual_shape = tuple(tensor.shape[1:]) + if actual_shape == expected_shape: + scores += 1 + self._record(name, 1, 1, []) + else: + self._record(name, 0, 1, [(expected_shape, actual_shape)]) + total += 1 + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except KeyError: + if debug: + print(f" {name}: SKIP (not found)") + + return scores, total + + # ========================================================================= + # INTEGRATION TESTS (Multi-circuit chains) + # ========================================================================= + + def _pop_modN(self, pop: Dict, pop_size: int, val_bits: torch.Tensor, + modulus: int) -> torch.Tensor: + """Drive the bit-cascade modular.mod{N} divisibility detector. + + Returns a (pop_size,) tensor: 1 iff the 8-bit value (MSB-first bits in + val_bits) is divisible by ``modulus``. Walks the per-multiple match + gates (modular.modN.eq.k{val}.bit{i}.match -> .all -> top-level OR). + """ + ks = [k for k in range(256) if k % modulus == 0] + alls = [] + for k in ks: + matches = [] + for i in range(8): + w = pop[f'modular.mod{modulus}.eq.k{k}.bit{i}.match.weight'].view(pop_size, 1) + b = pop[f'modular.mod{modulus}.eq.k{k}.bit{i}.match.bias'].view(pop_size) + matches.append(heaviside(val_bits[i] * w[:, 0] + b)) + all_inp = torch.stack(matches, dim=-1) + w_all = pop[f'modular.mod{modulus}.eq.k{k}.all.weight'].view(pop_size, 8) + b_all = pop[f'modular.mod{modulus}.eq.k{k}.all.bias'].view(pop_size) + alls.append(heaviside((all_inp * w_all).sum(-1) + b_all)) + top_inp = torch.stack(alls, dim=-1) + w_top = pop[f'modular.mod{modulus}.weight'].view(pop_size, len(ks)) + b_top = pop[f'modular.mod{modulus}.bias'].view(pop_size) + return heaviside((top_inp * w_top).sum(-1) + b_top) + + def _pop_cmp8bit(self, pop: Dict, pop_size: int, + a_bits: torch.Tensor, b_bits: torch.Tensor, + kind: str) -> torch.Tensor: + """Drive the bit-cascade comparator (cmp8bit) over a population. + + Returns a (pop_size,) tensor of heaviside outputs for the requested + comparison kind ('gt' | 'lt' | 'eq'). Bit 0 is MSB. + """ + def apply(name: str, inp: torch.Tensor, fan_in: int) -> torch.Tensor: + w = pop[f'{name}.weight'].view(pop_size, fan_in) + b = pop[f'{name}.bias'].view(pop_size) + return heaviside((inp * w).sum(-1) + b) + + # Per-bit primitives. + bit_gt, bit_lt, bit_eq = [], [], [] + for i in range(8): + ab = torch.stack([a_bits[i], b_bits[i]]) + bit_gt.append(apply(f'arithmetic.cmp8bit.bit{i}.gt', ab, 2)) + bit_lt.append(apply(f'arithmetic.cmp8bit.bit{i}.lt', ab, 2)) + eq_and = apply(f'arithmetic.cmp8bit.bit{i}.eq.layer1.and', ab, 2) + eq_nor = apply(f'arithmetic.cmp8bit.bit{i}.eq.layer1.nor', ab, 2) + eq_in = torch.stack([eq_and, eq_nor], dim=-1) + w = pop[f'arithmetic.cmp8bit.bit{i}.eq.weight'].view(pop_size, 2) + b = pop[f'arithmetic.cmp8bit.bit{i}.eq.bias'].view(pop_size) + bit_eq.append(heaviside((eq_in * w).sum(-1) + b)) + + # Cascade. + cas_gt = [bit_gt[0]] + cas_lt = [bit_lt[0]] + for i in range(1, 8): + eq_pref_in = torch.stack(bit_eq[:i], dim=-1) + w_pref = pop[f'arithmetic.cmp8bit.cascade.eq_prefix.bit{i}.weight'].view(pop_size, i) + b_pref = pop[f'arithmetic.cmp8bit.cascade.eq_prefix.bit{i}.bias'].view(pop_size) + eq_pref = heaviside((eq_pref_in * w_pref).sum(-1) + b_pref) + cas_in = torch.stack([eq_pref, bit_gt[i]], dim=-1) + w_g = pop[f'arithmetic.cmp8bit.cascade.gt.bit{i}.weight'].view(pop_size, 2) + b_g = pop[f'arithmetic.cmp8bit.cascade.gt.bit{i}.bias'].view(pop_size) + cas_gt.append(heaviside((cas_in * w_g).sum(-1) + b_g)) + cas_in_lt = torch.stack([eq_pref, bit_lt[i]], dim=-1) + w_l = pop[f'arithmetic.cmp8bit.cascade.lt.bit{i}.weight'].view(pop_size, 2) + b_l = pop[f'arithmetic.cmp8bit.cascade.lt.bit{i}.bias'].view(pop_size) + cas_lt.append(heaviside((cas_in_lt * w_l).sum(-1) + b_l)) + + if kind == 'gt': + inp = torch.stack(cas_gt, dim=-1) + return apply('arithmetic.greaterthan8bit', inp, 8) + if kind == 'lt': + inp = torch.stack(cas_lt, dim=-1) + return apply('arithmetic.lessthan8bit', inp, 8) + if kind == 'eq': + inp = torch.stack(bit_eq, dim=-1) + return apply('arithmetic.equality8bit', inp, 8) + raise ValueError(kind) + + def _test_integration(self, pop: Dict, debug: bool) -> Tuple[torch.Tensor, int]: + """Test complex operations that chain multiple circuit families.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total = 0 + + if debug: + print("\n=== INTEGRATION TESTS ===") + + # Test 1: ADD then compare (A + B > C?) + # Uses: ripple carry adder + comparator + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + tests = [(10, 20, 25), (100, 50, 200), (255, 1, 0), (0, 0, 1)] + for a, b, c in tests: + sum_val = (a + b) & 0xFF + expected = float(sum_val > c) + + # Compute sum bits + sum_bits = torch.tensor([((sum_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + c_bits = torch.tensor([((c >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # Drive sum_bits vs c_bits through the bit-cascade comparator. + out = self._pop_cmp8bit(pop, pop_size, sum_bits, c_bits, 'gt') + correct = (out == expected).float() + op_scores += correct + op_total += 1 + + scores += op_scores + total += op_total + self._record('integration.add_then_compare', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" integration.add_then_compare: SKIP ({e})") + + # Test 2: MUL then MOD (A * B mod 3 == 0?) + # Uses: partial products + modular arithmetic concept + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + tests = [(3, 5), (4, 6), (7, 11), (9, 9)] + for a, b in tests: + product = (a * b) & 0xFF + expected = float(product % 3 == 0) + + # Drive product bits through the bit-cascade mod3 detector; + # output is 1 iff product is divisible by 3. + prod_bits = torch.tensor([((product >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + out = self._pop_modN(pop, pop_size, prod_bits, 3) + op_scores += (out == expected).float() + op_total += 1 + + scores += op_scores + total += op_total + self._record('integration.mul_then_mod', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" integration.mul_then_mod: SKIP ({e})") + + # Test 3: Shift then AND (SHL(A) & B) + # Uses: shift + bitwise AND + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + tests = [(0b10101010, 0b11110000), (0b00001111, 0b01010101), (0xFF, 0x0F)] + for a, b in tests: + shifted_a = (a << 1) & 0xFF + expected = shifted_a & b + + a_bits = torch.tensor([((a >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + b_bits = torch.tensor([((b >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # Apply SHL + shifted_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.shl.bit{bit}.weight'].view(pop_size) + bias = pop[f'alu.alu8bit.shl.bit{bit}.bias'].view(pop_size) + if bit < 7: + inp = a_bits[bit + 1].expand(pop_size) + else: + inp = torch.zeros(pop_size, device=self.device) + shifted_bits.append(heaviside(inp * w + bias)) + + # Apply AND + and_bits = [] + w_and = pop['alu.alu8bit.and.weight'].view(pop_size, 8, 2) + b_and = pop['alu.alu8bit.and.bias'].view(pop_size, 8) + for bit in range(8): + inp = torch.stack([shifted_bits[bit], b_bits[bit].expand(pop_size)], dim=-1) + and_bits.append(heaviside((inp * w_and[:, bit]).sum(-1) + b_and[:, bit])) + + out_val = torch.zeros(pop_size, device=self.device) + for i in range(8): + out_val += and_bits[i] * (1 << (7 - i)) + op_scores += (out_val == expected).float() + op_total += 1 + + scores += op_scores + total += op_total + self._record('integration.shift_then_and', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" integration.shift_then_and: SKIP ({e})") + + # Test 4: SUB then conditional (A - B, if result < 0 then NEG) + # Uses: subtractor + comparator + conditional logic + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + tests = [(50, 30), (30, 50), (100, 100), (0, 1)] + for a, b in tests: + diff = (a - b) & 0xFF + is_negative = a < b + expected = (-diff & 0xFF) if is_negative else diff + + # Just verify the subtraction works correctly + # (Full conditional logic would require control flow) + a_bits = torch.tensor([((a >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + b_bits = torch.tensor([((b >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + + # Drive a_bits vs b_bits through the bit-cascade LT comparator. + lt_out = self._pop_cmp8bit(pop, pop_size, a_bits, b_bits, 'lt') + + op_scores += (lt_out == float(is_negative)).float() + op_total += 1 + + scores += op_scores + total += op_total + self._record('integration.sub_then_conditional', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" integration.sub_then_conditional: SKIP ({e})") + + # Test 5: Complex expression: ((A + B) * 2) & 0xF0 + # Uses: adder + SHL + AND + try: + op_scores = torch.zeros(pop_size, device=self.device) + op_total = 0 + + tests = [(10, 20), (50, 50), (127, 1), (0, 0)] + for a, b in tests: + sum_val = (a + b) & 0xFF + doubled = (sum_val << 1) & 0xFF + expected = doubled & 0xF0 + + sum_bits = torch.tensor([((sum_val >> (7 - i)) & 1) for i in range(8)], + device=self.device, dtype=torch.float32) + mask_bits = torch.tensor([1, 1, 1, 1, 0, 0, 0, 0], + device=self.device, dtype=torch.float32) + + # Apply SHL + shifted_bits = [] + for bit in range(8): + w = pop[f'alu.alu8bit.shl.bit{bit}.weight'].view(pop_size) + bias = pop[f'alu.alu8bit.shl.bit{bit}.bias'].view(pop_size) + if bit < 7: + inp = sum_bits[bit + 1].expand(pop_size) + else: + inp = torch.zeros(pop_size, device=self.device) + shifted_bits.append(heaviside(inp * w + bias)) + + # Apply AND with mask + w_and = pop['alu.alu8bit.and.weight'].view(pop_size, 8, 2) + b_and = pop['alu.alu8bit.and.bias'].view(pop_size, 8) + result_bits = [] + for bit in range(8): + inp = torch.stack([shifted_bits[bit], mask_bits[bit].expand(pop_size)], dim=-1) + result_bits.append(heaviside((inp * w_and[:, bit]).sum(-1) + b_and[:, bit])) + + out_val = torch.zeros(pop_size, device=self.device) + for i in range(8): + out_val += result_bits[i] * (1 << (7 - i)) + op_scores += (out_val == expected).float() + op_total += 1 + + scores += op_scores + total += op_total + self._record('integration.complex_expr', int(op_scores[0].item()), op_total, []) + if debug: + r = self.results[-1] + print(f" {r.name}: {r.passed}/{r.total} {'PASS' if r.success else 'FAIL'}") + except (KeyError, RuntimeError) as e: + if debug: + print(f" integration.complex_expr: SKIP ({e})") + + return scores, total + + # ========================================================================= + # MAIN EVALUATE + # ========================================================================= + + def evaluate(self, population: Dict[str, torch.Tensor], debug: bool = False) -> torch.Tensor: + """ + Evaluate population fitness with per-circuit reporting. + + Args: + population: Dict of tensors, each with shape [pop_size, ...] + debug: If True, print per-circuit results + + Returns: + Tensor of fitness scores [pop_size], normalized to [0, 1] + """ + self.results = [] + self.category_scores = {} + + pop_size = next(iter(population.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + # Boolean gates + s, t = self._test_boolean_gates(population, debug) + scores += s + total_tests += t + self.category_scores['boolean'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Half adder + s, t = self._test_halfadder(population, debug) + scores += s + total_tests += t + self.category_scores['halfadder'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Full adder + s, t = self._test_fulladder(population, debug) + scores += s + total_tests += t + self.category_scores['fulladder'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Ripple carry adders + for bits in [2, 4, 8]: + s, t = self._test_ripplecarry(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'ripplecarry{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # 16/32-bit circuits (if present) + for bits in [16, 32]: + if f'arithmetic.ripplecarry{bits}bit.fa0.ha1.sum.layer1.or.weight' in population: + if debug: + print(f"\n{'=' * 60}") + print(f" {bits}-BIT CIRCUITS") + print(f"{'=' * 60}") + + s, t = self._test_ripplecarry(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'ripplecarry{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_comparators_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'comparators{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'arithmetic.sub{bits}bit.not_b.bit0.weight' in population: + s, t = self._test_subtractor_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'subtractor{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'alu.alu{bits}bit.and.bit0.weight' in population: + s, t = self._test_bitwise_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'bitwise{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'alu.alu{bits}bit.shl.bit0.weight' in population: + s, t = self._test_shifts_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'shifts{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'alu.alu{bits}bit.inc.bit0.xor.layer1.or.weight' in population: + s, t = self._test_inc_dec_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'incdec{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'alu.alu{bits}bit.neg.not.bit0.weight' in population: + s, t = self._test_neg_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'neg{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'combinational.barrelshifter{bits}.layer0.bit0.not_sel.weight' in population: + s, t = self._test_barrel_shifter_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'barrelshifter{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if f'combinational.priorityencoder{bits}.valid.weight' in population: + s, t = self._test_priority_encoder_nbits(population, bits, debug) + scores += s + total_tests += t + self.category_scores[f'priorityencoder{bits}'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # 3-operand adder + s, t = self._test_add3(population, debug) + scores += s + total_tests += t + self.category_scores['add3'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Order of operations (A + B × C) + s, t = self._test_expr_add_mul(population, debug) + scores += s + total_tests += t + self.category_scores['expr_add_mul'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Comparators + s, t = self._test_comparators(population, debug) + scores += s + total_tests += t + self.category_scores['comparators'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Threshold gates + s, t = self._test_threshold_gates(population, debug) + scores += s + total_tests += t + self.category_scores['threshold'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Modular arithmetic + s, t = self._test_modular_all(population, debug) + scores += s + total_tests += t + self.category_scores['modular'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Pattern recognition + s, t = self._test_patterns(population, debug) + scores += s + total_tests += t + self.category_scores['patterns'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Error detection + s, t = self._test_error_detection(population, debug) + scores += s + total_tests += t + self.category_scores['error_detection'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Combinational + s, t = self._test_combinational(population, debug) + scores += s + total_tests += t + self.category_scores['combinational'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Control flow + s, t = self._test_control_flow(population, debug) + scores += s + total_tests += t + self.category_scores['control'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # ALU + s, t = self._test_alu_ops(population, debug) + scores += s + total_tests += t + self.category_scores['alu'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Manifest + s, t = self._test_manifest(population, debug) + scores += s + total_tests += t + self.category_scores['manifest'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Memory + s, t = self._test_memory(population, debug) + scores += s + total_tests += t + self.category_scores['memory'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Float16 circuits (if present) + if 'float16.unpack.bit0.weight' in population: + if debug: + print(f"\n{'=' * 60}") + print(f" FLOAT16 CIRCUITS") + print(f"{'=' * 60}") + + s, t = self._test_float_unpack_pack(population, 'float16', 16, debug) + scores += s + total_tests += t + self.category_scores['float16_unpack_pack'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_classify(population, 'float16', 5, 10, debug) + scores += s + total_tests += t + self.category_scores['float16_classify'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_cmp_composed(population, 'float16', 5, 10, debug) + scores += s + total_tests += t + self.category_scores['float16_cmp_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_arith_composed(population, 'float16', 'mul', + float_mul_oracle, 5, 10, debug) + scores += s + total_tests += t + self.category_scores['float16_mul_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_arith_composed(population, 'float16', 'div', + float_div_oracle, 5, 10, debug) + scores += s + total_tests += t + self.category_scores['float16_div_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_arith_composed(population, 'float16', 'add', + float_add_oracle, 5, 10, debug) + scores += s + total_tests += t + self.category_scores['float16_add_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float16_core(population, debug) + scores += s + total_tests += t + self.category_scores['float16_core'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float16.add.pl.gt.weight' in population: + s, t = self._test_float16_add(population, debug) + scores += s + total_tests += t + self.category_scores['float16_add'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float16.mul.sign_xor.layer1.or.weight' in population: + s, t = self._test_float16_mul(population, debug) + scores += s + total_tests += t + self.category_scores['float16_mul'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float16.div.sign_xor.layer1.or.weight' in population: + s, t = self._test_float16_div(population, debug) + scores += s + total_tests += t + self.category_scores['float16_div'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float16.cmp.a.exp_max.weight' in population: + s, t = self._test_float16_cmp(population, debug) + scores += s + total_tests += t + self.category_scores['float16_cmp'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Float32 circuits (if present) + if 'float32.unpack.bit0.weight' in population: + if debug: + print(f"\n{'=' * 60}") + print(f" FLOAT32 CIRCUITS") + print(f"{'=' * 60}") + + s, t = self._test_float_unpack_pack(population, 'float32', 32, debug) + scores += s + total_tests += t + self.category_scores['float32_unpack_pack'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_classify(population, 'float32', 8, 23, debug) + scores += s + total_tests += t + self.category_scores['float32_classify'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_cmp_composed(population, 'float32', 8, 23, debug) + scores += s + total_tests += t + self.category_scores['float32_cmp_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_arith_composed(population, 'float32', 'mul', + float_mul_oracle, 8, 23, debug) + scores += s + total_tests += t + self.category_scores['float32_mul_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_arith_composed(population, 'float32', 'div', + float_div_oracle, 8, 23, debug) + scores += s + total_tests += t + self.category_scores['float32_div_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float_arith_composed(population, 'float32', 'add', + float_add_oracle, 8, 23, debug) + scores += s + total_tests += t + self.category_scores['float32_add_composed'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + s, t = self._test_float32_core(population, debug) + scores += s + total_tests += t + self.category_scores['float32_core'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float32.add.pl.gt.weight' in population: + s, t = self._test_float32_add(population, debug) + scores += s + total_tests += t + self.category_scores['float32_add'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float32.mul.sign_xor.layer1.or.weight' in population: + s, t = self._test_float32_mul(population, debug) + scores += s + total_tests += t + self.category_scores['float32_mul'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float32.div.sign_xor.layer1.or.weight' in population: + s, t = self._test_float32_div(population, debug) + scores += s + total_tests += t + self.category_scores['float32_div'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + if 'float32.cmp.a.exp_max.weight' in population: + s, t = self._test_float32_cmp(population, debug) + scores += s + total_tests += t + self.category_scores['float32_cmp'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + # Cross-family integration tests (chain ripple-carry, comparator, + # modular, shifts, subtractor). Each test is internally guarded with + # try/except, so unsupported variants silently skip individual tests. + if 'arithmetic.cmp8bit.bit0.gt.weight' in population: + s, t = self._test_integration(population, debug) + scores += s + total_tests += t + self.category_scores['integration'] = (s[0].item() if pop_size == 1 else s.mean().item(), t) + + self.total_tests = total_tests + + if debug: + print("\n" + "=" * 60) + print("CATEGORY SUMMARY") + print("=" * 60) + for cat, (got, expected) in sorted(self.category_scores.items()): + pct = 100 * got / expected if expected > 0 else 0 + status = "PASS" if got == expected else "FAIL" + print(f" {cat:20} {int(got):6}/{expected:6} ({pct:6.2f}%) [{status}]") + + print("\n" + "=" * 60) + print("CIRCUIT FAILURES") + print("=" * 60) + failed = [r for r in self.results if not r.success] + if failed: + for r in failed[:20]: + print(f" {r.name}: {r.passed}/{r.total}") + if r.failures: + print(f" First failure: {r.failures[0]}") + if len(failed) > 20: + print(f" ... and {len(failed) - 20} more") + else: + print(" None!") + + return scores / total_tests if total_tests > 0 else scores + + +def main(): + parser = argparse.ArgumentParser(description='Unified Evaluation Suite for 8-bit Threshold Computer') + parser.add_argument('--model', type=str, default=MODEL_PATH, help='Path to safetensors model') + parser.add_argument('--device', type=str, default='cuda', help='Device: cuda or cpu') + parser.add_argument('--pop_size', type=int, default=1, help='Population size for batched evaluation') + parser.add_argument('--quiet', action='store_true', help='Suppress detailed output') + parser.add_argument('--cpu-test', action='store_true', help='Run CPU smoke test (LOAD, ADD, STORE, HALT)') + args = parser.parse_args() + + if args.cpu_test: + return run_smoke_test() + + print("=" * 70) + print(" UNIFIED EVALUATION SUITE") + print("=" * 70) + + print(f"\nLoading model from {args.model}...") + model = load_model(args.model) + print(f" Loaded {len(model)} tensors, {sum(t.numel() for t in model.values()):,} params") + + print(f"\nInitializing evaluator on {args.device}...") + evaluator = BatchedFitnessEvaluator(device=args.device, model_path=args.model) + + print(f"\nCreating population (size {args.pop_size})...") + population = create_population(model, pop_size=args.pop_size, device=args.device) + + print("\nRunning evaluation...") + if args.device == 'cuda': + torch.cuda.synchronize() + start = time.perf_counter() + + fitness = evaluator.evaluate(population, debug=not args.quiet) + + if args.device == 'cuda': + torch.cuda.synchronize() + elapsed = time.perf_counter() - start + + print("\n" + "=" * 70) + print("RESULTS") + print("=" * 70) + + if args.pop_size == 1: + print(f" Fitness: {fitness[0].item():.6f}") + else: + print(f" Mean Fitness: {fitness.mean().item():.6f}") + print(f" Min Fitness: {fitness.min().item():.6f}") + print(f" Max Fitness: {fitness.max().item():.6f}") + + print(f" Total tests: {evaluator.total_tests}") + print(f" Time: {elapsed * 1000:.2f} ms") + + if args.pop_size > 1: + print(f" Throughput: {args.pop_size / elapsed:.0f} evals/sec") + perfect = (fitness >= 0.9999).sum().item() + print(f" Perfect (>=99.99%): {perfect}/{args.pop_size}") + + if fitness[0].item() >= 0.9999: + print("\n STATUS: PASS") + return 0 + else: + failed_count = int((1 - fitness[0].item()) * evaluator.total_tests) + print(f"\n STATUS: FAIL ({failed_count} tests failed)") + return 1 + + +if __name__ == '__main__': + exit(main())