diff --git "a/tests/iron_eval.py" "b/tests/iron_eval.py" new file mode 100644--- /dev/null +++ "b/tests/iron_eval.py" @@ -0,0 +1,4020 @@ +""" +IRON EVAL - COMPREHENSIVE +========================= +Complete fitness evaluation for ALL circuits in the threshold computer. +108 circuits, no placeholders, no shortcuts. + +GPU-optimized for population-based evolution. +Target: ~40GB VRAM on RTX 6000 Ada (4M population) +""" + +import torch +from typing import Dict, Tuple +from safetensors import safe_open + + +def load_model(base_path: str = "D:/8bit-threshold-computer") -> Dict[str, torch.Tensor]: + """Load model from safetensors.""" + f = safe_open(f"{base_path}/neural_computer.safetensors", framework='numpy') + tensors = {} + for name in f.keys(): + tensors[name] = torch.tensor(f.get_tensor(name)).float() + return tensors + + +def heaviside(x: torch.Tensor) -> torch.Tensor: + """Threshold activation: 1 if x >= 0, else 0.""" + return (x >= 0).float() + + +class BatchedFitnessEvaluator: + """ + GPU-batched fitness evaluator. Tests ALL circuits comprehensively. + """ + + def __init__(self, device='cuda'): + self.device = device + self._setup_tests() + + def _setup_tests(self): + """Pre-compute all test vectors.""" + 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 - comprehensive set + 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 - comprehensive with bit boundaries + 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), + (200,100), (100,200), (1,2), (2,1), + (1,2), (2,1), (2,4), (4,2), (4,8), (8,4), + (8,16), (16,8), (16,32), (32,16), (32,64), (64,32), + (64,128), (128,64), + (1,1), (2,2), (4,4), (8,8), (16,16), (32,32), (64,64), (128,128), + (7,8), (8,7), (9,8), (8,9), + (15,16), (16,15), (17,16), (16,17), + (31,32), (32,31), (33,32), (32,33), + (63,64), (64,63), (65,64), (64,65), + (127,128), (128,127), (129,128), (128,129), + ] + 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) + self.comp_a_bits = torch.stack([((self.comp_a >> (7-i)) & 1).float() for i in range(8)], dim=1) + self.comp_b_bits = torch.stack([((self.comp_b >> (7-i)) & 1).float() for i in range(8)], dim=1) + + # Modular test values + self.mod_test = torch.arange(0, 256, device=d, dtype=torch.long) + self.mod_test_bits = torch.stack([((self.mod_test >> (7-i)) & 1).float() for i in range(8)], dim=1) + + # ========================================================================= + # BOOLEAN GATES + # ========================================================================= + + def _test_single_gate(self, pop: Dict, gate: str, inputs: torch.Tensor, + expected: torch.Tensor) -> torch.Tensor: + """Test single-layer boolean gate.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop[f'boolean.{gate}.weight'].view(pop_size, -1) + b = pop[f'boolean.{gate}.bias'].view(pop_size) + out = heaviside(inputs @ w.T + b) + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_twolayer_gate(self, pop: Dict, prefix: str, inputs: torch.Tensor, + expected: torch.Tensor) -> torch.Tensor: + """Test two-layer gate (XOR, XNOR, BIIMPLIES) - boolean naming (neuron1/neuron2).""" + pop_size = next(iter(pop.values())).shape[0] + + w1_a = pop[f'{prefix}.layer1.neuron1.weight'].view(pop_size, -1) + b1_a = pop[f'{prefix}.layer1.neuron1.bias'].view(pop_size) + w1_b = pop[f'{prefix}.layer1.neuron2.weight'].view(pop_size, -1) + b1_b = pop[f'{prefix}.layer1.neuron2.bias'].view(pop_size) + + h_a = heaviside(inputs @ w1_a.T + b1_a) + h_b = heaviside(inputs @ w1_b.T + b1_b) + hidden = torch.stack([h_a, h_b], dim=2) + + w2 = pop[f'{prefix}.layer2.weight'].view(pop_size, -1) + b2 = pop[f'{prefix}.layer2.bias'].view(pop_size) + out = heaviside((hidden * w2.unsqueeze(0)).sum(2) + b2.unsqueeze(0)) + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_xor_gate_ornand(self, pop: Dict, prefix: str, inputs: torch.Tensor, + expected: torch.Tensor) -> torch.Tensor: + """Test two-layer XOR gate - arithmetic naming (or/nand).""" + pop_size = next(iter(pop.values())).shape[0] + + w1_or = pop[f'{prefix}.layer1.or.weight'].view(pop_size, -1) + b1_or = pop[f'{prefix}.layer1.or.bias'].view(pop_size) + w1_nand = pop[f'{prefix}.layer1.nand.weight'].view(pop_size, -1) + b1_nand = pop[f'{prefix}.layer1.nand.bias'].view(pop_size) + + h_or = heaviside(inputs @ w1_or.T + b1_or) + h_nand = heaviside(inputs @ w1_nand.T + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=2) + + w2 = pop[f'{prefix}.layer2.weight'].view(pop_size, -1) + b2 = pop[f'{prefix}.layer2.bias'].view(pop_size) + out = heaviside((hidden * w2.unsqueeze(0)).sum(2) + b2.unsqueeze(0)) + + return (out == expected.unsqueeze(1)).float().sum(0) + + # ========================================================================= + # ARITHMETIC - ADDERS + # ========================================================================= + + def _test_halfadder(self, pop: Dict) -> torch.Tensor: + """Test half adder: sum and carry.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + scores += self._test_xor_gate_ornand(pop, 'arithmetic.halfadder.sum', + self.tt2, self.expected['ha_sum']) + # Carry (AND) + w = pop['arithmetic.halfadder.carry.weight'].view(pop_size, -1) + b = pop['arithmetic.halfadder.carry.bias'].view(pop_size) + out = heaviside(self.tt2 @ w.T + b) + scores += (out == self.expected['ha_carry'].unsqueeze(1)).float().sum(0) + + return scores + + def _test_fulladder(self, pop: Dict) -> torch.Tensor: + """Test full adder circuit.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for i, (a, b, cin) in enumerate([(0,0,0), (0,0,1), (0,1,0), (0,1,1), + (1,0,0), (1,0,1), (1,1,0), (1,1,1)]): + inp_ab = torch.tensor([[float(a), float(b)]], device=self.device) + + # HA1 + ha1_sum = self._eval_xor(pop, 'arithmetic.fulladder.ha1.sum', inp_ab) + w_c1 = pop['arithmetic.fulladder.ha1.carry.weight'].view(pop_size, -1) + b_c1 = pop['arithmetic.fulladder.ha1.carry.bias'].view(pop_size) + ha1_carry = heaviside(inp_ab @ w_c1.T + b_c1) + + # HA2 + inp_ha2 = torch.stack([ha1_sum.squeeze(0), torch.full((pop_size,), float(cin), device=self.device)], dim=1) + + w1_or = pop['arithmetic.fulladder.ha2.sum.layer1.or.weight'].view(pop_size, -1) + b1_or = pop['arithmetic.fulladder.ha2.sum.layer1.or.bias'].view(pop_size) + w1_nand = pop['arithmetic.fulladder.ha2.sum.layer1.nand.weight'].view(pop_size, -1) + b1_nand = pop['arithmetic.fulladder.ha2.sum.layer1.nand.bias'].view(pop_size) + w2 = pop['arithmetic.fulladder.ha2.sum.layer2.weight'].view(pop_size, -1) + b2 = pop['arithmetic.fulladder.ha2.sum.layer2.bias'].view(pop_size) + + h_or = heaviside((inp_ha2 * w1_or).sum(1) + b1_or) + h_nand = heaviside((inp_ha2 * w1_nand).sum(1) + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=1) + ha2_sum = heaviside((hidden * w2).sum(1) + b2) + + w_c2 = pop['arithmetic.fulladder.ha2.carry.weight'].view(pop_size, -1) + b_c2 = pop['arithmetic.fulladder.ha2.carry.bias'].view(pop_size) + ha2_carry = heaviside((inp_ha2 * w_c2).sum(1) + b_c2) + + # Carry OR + inp_cout = torch.stack([ha1_carry.squeeze(0), ha2_carry], dim=1) + w_cor = pop['arithmetic.fulladder.carry_or.weight'].view(pop_size, -1) + b_cor = pop['arithmetic.fulladder.carry_or.bias'].view(pop_size) + cout = heaviside((inp_cout * w_cor).sum(1) + b_cor) + + scores += (ha2_sum == self.expected['fa_sum'][i]).float() + scores += (cout == self.expected['fa_cout'][i]).float() + + return scores + + def _eval_xor(self, pop: Dict, prefix: str, inputs: torch.Tensor) -> torch.Tensor: + """Evaluate XOR gate for given inputs.""" + pop_size = next(iter(pop.values())).shape[0] + + w1_or = pop[f'{prefix}.layer1.or.weight'].view(pop_size, -1) + b1_or = pop[f'{prefix}.layer1.or.bias'].view(pop_size) + w1_nand = pop[f'{prefix}.layer1.nand.weight'].view(pop_size, -1) + b1_nand = pop[f'{prefix}.layer1.nand.bias'].view(pop_size) + w2 = pop[f'{prefix}.layer2.weight'].view(pop_size, -1) + b2 = pop[f'{prefix}.layer2.bias'].view(pop_size) + + h_or = heaviside(inputs @ w1_or.T + b1_or) + h_nand = heaviside(inputs @ w1_nand.T + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=2) + return heaviside((hidden * w2.unsqueeze(0)).sum(2) + b2.unsqueeze(0)) + + def _eval_single_fa(self, pop: Dict, prefix: str, a: torch.Tensor, + b: torch.Tensor, cin: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """Evaluate a single full adder.""" + pop_size = a.shape[0] + inp_ab = torch.stack([a, b], dim=1) + + # HA1 XOR + w1_or = pop[f'{prefix}.ha1.sum.layer1.or.weight'].view(pop_size, -1) + b1_or = pop[f'{prefix}.ha1.sum.layer1.or.bias'].view(pop_size) + w1_nand = pop[f'{prefix}.ha1.sum.layer1.nand.weight'].view(pop_size, -1) + b1_nand = pop[f'{prefix}.ha1.sum.layer1.nand.bias'].view(pop_size) + w1_l2 = pop[f'{prefix}.ha1.sum.layer2.weight'].view(pop_size, -1) + b1_l2 = pop[f'{prefix}.ha1.sum.layer2.bias'].view(pop_size) + + h_or = heaviside((inp_ab * w1_or).sum(1) + b1_or) + h_nand = heaviside((inp_ab * w1_nand).sum(1) + b1_nand) + hidden1 = torch.stack([h_or, h_nand], dim=1) + ha1_sum = heaviside((hidden1 * w1_l2).sum(1) + b1_l2) + + w_c1 = pop[f'{prefix}.ha1.carry.weight'].view(pop_size, -1) + b_c1 = pop[f'{prefix}.ha1.carry.bias'].view(pop_size) + ha1_carry = heaviside((inp_ab * w_c1).sum(1) + b_c1) + + # HA2 XOR + inp_ha2 = torch.stack([ha1_sum, cin], dim=1) + + w2_or = pop[f'{prefix}.ha2.sum.layer1.or.weight'].view(pop_size, -1) + b2_or = pop[f'{prefix}.ha2.sum.layer1.or.bias'].view(pop_size) + w2_nand = pop[f'{prefix}.ha2.sum.layer1.nand.weight'].view(pop_size, -1) + b2_nand = pop[f'{prefix}.ha2.sum.layer1.nand.bias'].view(pop_size) + w2_l2 = pop[f'{prefix}.ha2.sum.layer2.weight'].view(pop_size, -1) + b2_l2 = pop[f'{prefix}.ha2.sum.layer2.bias'].view(pop_size) + + h2_or = heaviside((inp_ha2 * w2_or).sum(1) + b2_or) + h2_nand = heaviside((inp_ha2 * w2_nand).sum(1) + b2_nand) + hidden2 = torch.stack([h2_or, h2_nand], dim=1) + ha2_sum = heaviside((hidden2 * w2_l2).sum(1) + b2_l2) + + w_c2 = pop[f'{prefix}.ha2.carry.weight'].view(pop_size, -1) + b_c2 = pop[f'{prefix}.ha2.carry.bias'].view(pop_size) + ha2_carry = heaviside((inp_ha2 * w_c2).sum(1) + b_c2) + + # Carry OR + inp_cout = torch.stack([ha1_carry, ha2_carry], dim=1) + w_cor = pop[f'{prefix}.carry_or.weight'].view(pop_size, -1) + b_cor = pop[f'{prefix}.carry_or.bias'].view(pop_size) + cout = heaviside((inp_cout * w_cor).sum(1) + b_cor) + + return ha2_sum, cout + + def _test_ripplecarry(self, pop: Dict, bits: int, test_cases: list) -> torch.Tensor: + """Test ripple carry adder of given bit width.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for a_val, b_val in test_cases: + # Extract bits + a_bits = [(a_val >> i) & 1 for i in range(bits)] + b_bits = [(b_val >> i) & 1 for i in range(bits)] + + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + + for i in range(bits): + a_i = torch.full((pop_size,), float(a_bits[i]), device=self.device) + b_i = torch.full((pop_size,), float(b_bits[i]), device=self.device) + sum_i, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry{bits}bit.fa{i}', a_i, b_i, carry) + sum_bits.append(sum_i) + + # Reconstruct result + result = sum(sum_bits[i] * (2**i) for i in range(bits)) + expected = (a_val + b_val) & ((1 << bits) - 1) + scores += (result == expected).float() + + return scores + + def _test_multiplier8x8(self, pop: Dict, debug: bool = False) -> torch.Tensor: + """Test 8x8 array multiplier.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + failures = [] + + test_pairs = [ + (0, 0), (1, 1), (2, 3), (3, 2), (7, 7), (8, 8), + (15, 15), (16, 16), (255, 1), (1, 255), (255, 255), + (12, 12), (10, 25), (17, 15), (128, 2), (64, 4), + (0, 255), (255, 0), (100, 100), (50, 200), (200, 50), + (127, 127), (128, 128), (85, 3), (170, 2), (99, 99), + (13, 17), (23, 29), (31, 33), (41, 43), (53, 59), + (61, 67), (71, 73), (79, 83), (89, 97), (101, 103), + (107, 109), (113, 127), (131, 137), (139, 149), (151, 157), + (163, 167), (173, 179), (181, 191), (193, 197), (199, 211), + (223, 227), (229, 233), (239, 241), (251, 1), (1, 251), + (2, 128), (4, 64), (8, 32), (32, 8), + (3, 85), (5, 51), (7, 36), + (9, 28), (11, 23), (13, 19), (15, 17) + ] + + for a_val, b_val in test_pairs: + expected = (a_val * b_val) & 0xFFFF + + a_bits = [(a_val >> i) & 1 for i in range(8)] + b_bits = [(b_val >> i) & 1 for i in range(8)] + + pp = [[torch.zeros(pop_size, device=self.device) for _ in range(8)] for _ in range(8)] + for row in range(8): + for col in range(8): + a_t = torch.full((pop_size,), float(a_bits[col]), device=self.device) + b_t = torch.full((pop_size,), float(b_bits[row]), device=self.device) + inp = torch.stack([a_t, b_t], dim=1) + w = pop[f'arithmetic.multiplier8x8.pp.r{row}.c{col}.weight'].view(pop_size, -1) + b = pop[f'arithmetic.multiplier8x8.pp.r{row}.c{col}.bias'].view(pop_size) + pp[row][col] = heaviside((inp * w).sum(1) + b) + + result_bits = [torch.zeros(pop_size, device=self.device) for _ in range(16)] + for col in range(8): + result_bits[col] = pp[0][col] + + for stage in range(7): + row_idx = stage + 1 + shift = row_idx + sum_width = 8 + stage + 1 + + new_result = [r.clone() for r in result_bits] + carry = torch.zeros(pop_size, device=self.device) + + for bit in range(sum_width): + if bit < shift: + pp_bit = torch.zeros(pop_size, device=self.device) + elif bit <= shift + 7: + pp_bit = pp[row_idx][bit - shift] + else: + pp_bit = torch.zeros(pop_size, device=self.device) + + prev_bit = result_bits[bit] if bit < 16 else torch.zeros(pop_size, device=self.device) + + prefix = f'arithmetic.multiplier8x8.stage{stage}.bit{bit}' + + inp_ab = torch.stack([prev_bit, pp_bit], dim=1) + w1_or = pop[f'{prefix}.ha1.sum.layer1.or.weight'].view(pop_size, -1) + b1_or = pop[f'{prefix}.ha1.sum.layer1.or.bias'].view(pop_size) + w1_nand = pop[f'{prefix}.ha1.sum.layer1.nand.weight'].view(pop_size, -1) + b1_nand = pop[f'{prefix}.ha1.sum.layer1.nand.bias'].view(pop_size) + w1_l2 = pop[f'{prefix}.ha1.sum.layer2.weight'].view(pop_size, -1) + b1_l2 = pop[f'{prefix}.ha1.sum.layer2.bias'].view(pop_size) + + h_or = heaviside((inp_ab * w1_or).sum(1) + b1_or) + h_nand = heaviside((inp_ab * w1_nand).sum(1) + b1_nand) + hidden1 = torch.stack([h_or, h_nand], dim=1) + ha1_sum = heaviside((hidden1 * w1_l2).sum(1) + b1_l2) + + w_c1 = pop[f'{prefix}.ha1.carry.weight'].view(pop_size, -1) + b_c1 = pop[f'{prefix}.ha1.carry.bias'].view(pop_size) + ha1_carry = heaviside((inp_ab * w_c1).sum(1) + b_c1) + + inp_ha2 = torch.stack([ha1_sum, carry], dim=1) + w2_or = pop[f'{prefix}.ha2.sum.layer1.or.weight'].view(pop_size, -1) + b2_or = pop[f'{prefix}.ha2.sum.layer1.or.bias'].view(pop_size) + w2_nand = pop[f'{prefix}.ha2.sum.layer1.nand.weight'].view(pop_size, -1) + b2_nand = pop[f'{prefix}.ha2.sum.layer1.nand.bias'].view(pop_size) + w2_l2 = pop[f'{prefix}.ha2.sum.layer2.weight'].view(pop_size, -1) + b2_l2 = pop[f'{prefix}.ha2.sum.layer2.bias'].view(pop_size) + + h2_or = heaviside((inp_ha2 * w2_or).sum(1) + b2_or) + h2_nand = heaviside((inp_ha2 * w2_nand).sum(1) + b2_nand) + hidden2 = torch.stack([h2_or, h2_nand], dim=1) + ha2_sum = heaviside((hidden2 * w2_l2).sum(1) + b2_l2) + + w_c2 = pop[f'{prefix}.ha2.carry.weight'].view(pop_size, -1) + b_c2 = pop[f'{prefix}.ha2.carry.bias'].view(pop_size) + ha2_carry = heaviside((inp_ha2 * w_c2).sum(1) + b_c2) + + inp_cout = torch.stack([ha1_carry, ha2_carry], dim=1) + w_cor = pop[f'{prefix}.carry_or.weight'].view(pop_size, -1) + b_cor = pop[f'{prefix}.carry_or.bias'].view(pop_size) + carry = heaviside((inp_cout * w_cor).sum(1) + b_cor) + + if bit < 16: + new_result[bit] = ha2_sum + + # Propagate carry to next bit position if within range + if sum_width < 16: + new_result[sum_width] = carry + + result_bits = new_result + + result = sum(result_bits[i] * (2**i) for i in range(16)) + passed = (result == expected).float() + scores += passed + if debug and pop_size == 1 and passed[0].item() == 0: + failures.append(f"MULT({a_val} * {b_val}) = {int(result[0].item())}, expected {expected}") + + if debug and failures: + print(f"\n Multiplier failures ({len(failures)}):") + for f in failures: + print(f" {f}") + + return scores + + # ========================================================================= + # ARITHMETIC - COMPARATORS + # ========================================================================= + + def _test_comparator(self, pop: Dict, name: str, op: str) -> torch.Tensor: + """Test 8-bit comparator.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop[f'arithmetic.{name}.comparator'].view(pop_size, -1) + + if op == 'gt': + diff = self.comp_a_bits - self.comp_b_bits + expected = (self.comp_a > self.comp_b).float() + elif op == 'lt': + diff = self.comp_b_bits - self.comp_a_bits + expected = (self.comp_a < self.comp_b).float() + elif op == 'geq': + diff = self.comp_a_bits - self.comp_b_bits + expected = (self.comp_a >= self.comp_b).float() + elif op == 'leq': + diff = self.comp_b_bits - self.comp_a_bits + expected = (self.comp_a <= self.comp_b).float() + + score = diff @ w.T + if op in ['geq', 'leq']: + out = (score >= 0).float() + else: + out = (score > 0).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_equality(self, pop: Dict) -> torch.Tensor: + """Test 8-bit equality circuit.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for i in range(len(self.comp_a)): + a_bits = self.comp_a_bits[i] + b_bits = self.comp_b_bits[i] + + # Compute XNOR for each bit pair + xnor_results = [] + for bit in range(8): + inp = torch.stack([ + torch.full((pop_size,), a_bits[bit].item(), device=self.device), + torch.full((pop_size,), b_bits[bit].item(), device=self.device) + ], dim=1) + + # XNOR = (a AND b) OR (NOR(a,b)) + w_and = pop[f'arithmetic.equality8bit.xnor{bit}.layer1.and.weight'].view(pop_size, -1) + b_and = pop[f'arithmetic.equality8bit.xnor{bit}.layer1.and.bias'].view(pop_size) + w_nor = pop[f'arithmetic.equality8bit.xnor{bit}.layer1.nor.weight'].view(pop_size, -1) + b_nor = pop[f'arithmetic.equality8bit.xnor{bit}.layer1.nor.bias'].view(pop_size) + w_l2 = pop[f'arithmetic.equality8bit.xnor{bit}.layer2.weight'].view(pop_size, -1) + b_l2 = pop[f'arithmetic.equality8bit.xnor{bit}.layer2.bias'].view(pop_size) + + h_and = heaviside((inp * w_and).sum(1) + b_and) + h_nor = heaviside((inp * w_nor).sum(1) + b_nor) + hidden = torch.stack([h_and, h_nor], dim=1) + xnor_out = heaviside((hidden * w_l2).sum(1) + b_l2) + xnor_results.append(xnor_out) + + # Final AND of all XNORs + xnor_stack = torch.stack(xnor_results, dim=1) + w_final = pop['arithmetic.equality8bit.final_and.weight'].view(pop_size, -1) + b_final = pop['arithmetic.equality8bit.final_and.bias'].view(pop_size) + eq_out = heaviside((xnor_stack * w_final).sum(1) + b_final) + + expected = (self.comp_a[i] == self.comp_b[i]).float() + scores += (eq_out == expected).float() + + return scores + + # ========================================================================= + # THRESHOLD GATES + # ========================================================================= + + def _test_threshold_kofn(self, pop: Dict, k: int, name: str) -> torch.Tensor: + """Test k-of-8 threshold gate.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop[f'threshold.{name}.weight'].view(pop_size, -1) + b = pop[f'threshold.{name}.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + popcounts = self.test_8bit_bits.sum(1) + expected = (popcounts >= k).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_majority(self, pop: Dict) -> torch.Tensor: + """Test majority gate (5+ of 8).""" + pop_size = next(iter(pop.values())).shape[0] + w = pop['threshold.majority.weight'].view(pop_size, -1) + b = pop['threshold.majority.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + popcounts = self.test_8bit_bits.sum(1) + expected = (popcounts >= 5).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_minority(self, pop: Dict) -> torch.Tensor: + """Test minority gate (3 or fewer of 8).""" + pop_size = next(iter(pop.values())).shape[0] + w = pop['threshold.minority.weight'].view(pop_size, -1) + b = pop['threshold.minority.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + popcounts = self.test_8bit_bits.sum(1) + expected = (popcounts <= 3).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_atleastk(self, pop: Dict, k: int) -> torch.Tensor: + """Test at-least-k threshold gate.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop[f'threshold.atleastk_{k}.weight'].view(pop_size, -1) + b = pop[f'threshold.atleastk_{k}.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + popcounts = self.test_8bit_bits.sum(1) + expected = (popcounts >= k).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_atmostk(self, pop: Dict, k: int) -> torch.Tensor: + """Test at-most-k threshold gate.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop[f'threshold.atmostk_{k}.weight'].view(pop_size, -1) + b = pop[f'threshold.atmostk_{k}.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + popcounts = self.test_8bit_bits.sum(1) + expected = (popcounts <= k).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_exactlyk(self, pop: Dict, k: int) -> torch.Tensor: + """Test exactly-k threshold gate (uses atleast AND atmost).""" + pop_size = next(iter(pop.values())).shape[0] + + # At least k + w_al = pop[f'threshold.exactlyk_{k}.atleast.weight'].view(pop_size, -1) + b_al = pop[f'threshold.exactlyk_{k}.atleast.bias'].view(pop_size) + atleast = heaviside(self.test_8bit_bits @ w_al.T + b_al) + + # At most k + w_am = pop[f'threshold.exactlyk_{k}.atmost.weight'].view(pop_size, -1) + b_am = pop[f'threshold.exactlyk_{k}.atmost.bias'].view(pop_size) + atmost = heaviside(self.test_8bit_bits @ w_am.T + b_am) + + # AND + combined = torch.stack([atleast, atmost], dim=2) + w_and = pop[f'threshold.exactlyk_{k}.and.weight'].view(pop_size, -1) + b_and = pop[f'threshold.exactlyk_{k}.and.bias'].view(pop_size) + out = heaviside((combined * w_and.unsqueeze(0)).sum(2) + b_and.unsqueeze(0)) + + popcounts = self.test_8bit_bits.sum(1) + expected = (popcounts == k).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + # ========================================================================= + # PATTERN RECOGNITION + # ========================================================================= + + def _test_popcount(self, pop: Dict) -> torch.Tensor: + """Test popcount (count of 1 bits).""" + pop_size = next(iter(pop.values())).shape[0] + w = pop['pattern_recognition.popcount.weight'].view(pop_size, -1) + b = pop['pattern_recognition.popcount.bias'].view(pop_size) + + out = (self.test_8bit_bits @ w.T + b) # No heaviside - this is a counter + expected = self.test_8bit_bits.sum(1) + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_allzeros(self, pop: Dict) -> torch.Tensor: + """Test all-zeros detector.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop['pattern_recognition.allzeros.weight'].view(pop_size, -1) + b = pop['pattern_recognition.allzeros.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + expected = (self.test_8bit == 0).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + def _test_allones(self, pop: Dict) -> torch.Tensor: + """Test all-ones detector.""" + pop_size = next(iter(pop.values())).shape[0] + w = pop['pattern_recognition.allones.weight'].view(pop_size, -1) + b = pop['pattern_recognition.allones.bias'].view(pop_size) + + out = heaviside(self.test_8bit_bits @ w.T + b) + expected = (self.test_8bit == 255).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + # ========================================================================= + # ERROR DETECTION + # ========================================================================= + + def _eval_xor_gate(self, pop: Dict, prefix: str, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """Evaluate XOR gate on batched inputs.""" + pop_size = next(iter(pop.values())).shape[0] + + w_or = pop[f'{prefix}.layer1.or.weight'].view(pop_size, -1) + b_or = pop[f'{prefix}.layer1.or.bias'].view(pop_size) + w_nand = pop[f'{prefix}.layer1.nand.weight'].view(pop_size, -1) + b_nand = pop[f'{prefix}.layer1.nand.bias'].view(pop_size) + w_and = pop[f'{prefix}.layer2.weight'].view(pop_size, -1) + b_and = pop[f'{prefix}.layer2.bias'].view(pop_size) + + inp = torch.stack([a, b], dim=2) + h_or = heaviside((inp * w_or.unsqueeze(0)).sum(2) + b_or.unsqueeze(0)) + h_nand = heaviside((inp * w_nand.unsqueeze(0)).sum(2) + b_nand.unsqueeze(0)) + hidden = torch.stack([h_or, h_nand], dim=2) + return heaviside((hidden * w_and.unsqueeze(0)).sum(2) + b_and.unsqueeze(0)) + + def _test_parity(self, pop: Dict, name: str, even: bool) -> torch.Tensor: + """Test parity checker/generator with XOR tree.""" + pop_size = next(iter(pop.values())).shape[0] + prefix = f'error_detection.{name}' + num_tests = self.test_8bit_bits.shape[0] + + bits = self.test_8bit_bits.unsqueeze(1).expand(-1, pop_size, -1) + + stage1 = [] + for i, (a, b) in enumerate([(0, 1), (2, 3), (4, 5), (6, 7)]): + xor_out = self._eval_xor_gate(pop, f'{prefix}.stage1.xor{i}', bits[:,:,a], bits[:,:,b]) + stage1.append(xor_out) + + stage2 = [] + stage2.append(self._eval_xor_gate(pop, f'{prefix}.stage2.xor0', stage1[0], stage1[1])) + stage2.append(self._eval_xor_gate(pop, f'{prefix}.stage2.xor1', stage1[2], stage1[3])) + + xor_all = self._eval_xor_gate(pop, f'{prefix}.stage3.xor0', stage2[0], stage2[1]) + + w_not = pop[f'{prefix}.output.not.weight'].view(pop_size, -1) + b_not = pop[f'{prefix}.output.not.bias'].view(pop_size) + out = heaviside(xor_all.unsqueeze(2) * w_not.unsqueeze(0) + b_not.view(1, pop_size, 1)).squeeze(2) + + popcounts = self.test_8bit_bits.sum(1) + if even: + expected = ((popcounts.long() % 2) == 0).float() + else: + expected = ((popcounts.long() % 2) == 1).float() + + return (out == expected.unsqueeze(1)).float().sum(0) + + # ========================================================================= + # MODULAR ARITHMETIC + # ========================================================================= + + def _get_divisible_sums(self, mod: int) -> list: + """Get sum values that indicate divisibility by mod.""" + weights = [(2**(7-i)) % mod for i in range(8)] + max_sum = sum(weights) + return [k for k in range(0, max_sum + 1) if k % mod == 0] + + def _test_modular(self, pop: Dict, mod: int) -> torch.Tensor: + """Test modular arithmetic circuit.""" + pop_size = next(iter(pop.values())).shape[0] + + if mod in [2, 4, 8]: + w = pop[f'modular.mod{mod}.weight'].view(pop_size, -1) + b = pop[f'modular.mod{mod}.bias'].view(pop_size) + out = heaviside(self.mod_test_bits @ w.T + b) + else: + divisible_sums = self._get_divisible_sums(mod) + num_detectors = len(divisible_sums) + + layer1_outputs = [] + for idx in range(num_detectors): + w_geq = pop[f'modular.mod{mod}.layer1.geq{idx}.weight'].view(pop_size, -1) + b_geq = pop[f'modular.mod{mod}.layer1.geq{idx}.bias'].view(pop_size) + w_leq = pop[f'modular.mod{mod}.layer1.leq{idx}.weight'].view(pop_size, -1) + b_leq = pop[f'modular.mod{mod}.layer1.leq{idx}.bias'].view(pop_size) + + geq = heaviside(self.mod_test_bits @ w_geq.T + b_geq) + leq = heaviside(self.mod_test_bits @ w_leq.T + b_leq) + layer1_outputs.append((geq, leq)) + + layer2_outputs = [] + for idx in range(num_detectors): + w_eq = pop[f'modular.mod{mod}.layer2.eq{idx}.weight'].view(pop_size, -1) + b_eq = pop[f'modular.mod{mod}.layer2.eq{idx}.bias'].view(pop_size) + geq, leq = layer1_outputs[idx] + combined = torch.stack([geq, leq], dim=2) + eq = heaviside((combined * w_eq.unsqueeze(0)).sum(2) + b_eq.unsqueeze(0)) + layer2_outputs.append(eq) + + layer2_stack = torch.stack(layer2_outputs, dim=2) + w_or = pop[f'modular.mod{mod}.layer3.or.weight'].view(pop_size, -1) + b_or = pop[f'modular.mod{mod}.layer3.or.bias'].view(pop_size) + out = heaviside((layer2_stack * w_or.unsqueeze(0)).sum(2) + b_or.unsqueeze(0)) + + expected = ((self.mod_test % mod) == 0).float() + return (out == expected.unsqueeze(1)).float().sum(0) + + # ========================================================================= + # COMBINATIONAL + # ========================================================================= + + def _test_mux2to1(self, pop: Dict) -> torch.Tensor: + """Test 2:1 multiplexer.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for a in [0, 1]: + for b in [0, 1]: + for sel in [0, 1]: + expected = a if sel == 1 else b + + a_t = torch.full((pop_size,), float(a), device=self.device) + b_t = torch.full((pop_size,), float(b), device=self.device) + sel_t = torch.full((pop_size,), float(sel), device=self.device) + + w_not = pop['combinational.multiplexer2to1.not_s.weight'].view(pop_size, -1) + b_not = pop['combinational.multiplexer2to1.not_s.bias'].view(pop_size) + not_sel = heaviside((sel_t.unsqueeze(1) * w_not).sum(1) + b_not) + + inp_a = torch.stack([a_t, sel_t], dim=1) + w_and_a = pop['combinational.multiplexer2to1.and1.weight'].view(pop_size, -1) + b_and_a = pop['combinational.multiplexer2to1.and1.bias'].view(pop_size) + and_a = heaviside((inp_a * w_and_a).sum(1) + b_and_a) + + inp_b = torch.stack([b_t, not_sel], dim=1) + w_and_b = pop['combinational.multiplexer2to1.and0.weight'].view(pop_size, -1) + b_and_b = pop['combinational.multiplexer2to1.and0.bias'].view(pop_size) + and_b = heaviside((inp_b * w_and_b).sum(1) + b_and_b) + + inp_or = torch.stack([and_a, and_b], dim=1) + w_or = pop['combinational.multiplexer2to1.or.weight'].view(pop_size, -1) + b_or = pop['combinational.multiplexer2to1.or.bias'].view(pop_size) + out = heaviside((inp_or * w_or).sum(1) + b_or) + + scores += (out == expected).float() + + return scores + + def _test_decoder3to8(self, pop: Dict) -> torch.Tensor: + """Test 3-to-8 decoder.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for val in range(8): + bits = [(val >> i) & 1 for i in range(3)] + inp = torch.tensor([[float(bits[2]), float(bits[1]), float(bits[0])]], device=self.device) + + # Test each output + for out_idx in range(8): + w = pop[f'combinational.decoder3to8.out{out_idx}.weight'].view(pop_size, -1) + b = pop[f'combinational.decoder3to8.out{out_idx}.bias'].view(pop_size) + out = heaviside(inp @ w.T + b) + expected = 1.0 if out_idx == val else 0.0 + scores += (out.squeeze() == expected).float() + + return scores + + def _test_encoder8to3(self, pop: Dict) -> torch.Tensor: + """Test 8-to-3 encoder (one-hot to binary).""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for val in range(8): + # One-hot input + inp = torch.zeros(1, 8, device=self.device) + inp[0, val] = 1.0 + + for bit in range(3): + w = pop[f'combinational.encoder8to3.bit{bit}.weight'].view(pop_size, -1) + b = pop[f'combinational.encoder8to3.bit{bit}.bias'].view(pop_size) + out = heaviside(inp @ w.T + b) + expected = float((val >> bit) & 1) + scores += (out.squeeze() == expected).float() + + return scores + + # ========================================================================= + # CONTROL FLOW (8-bit conditional MUX) + # ========================================================================= + + def _test_conditional_jump(self, pop: Dict, name: str) -> torch.Tensor: + """Test 8-bit conditional jump (MUX) circuit.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + # Test with a few representative 8-bit value pairs and conditions + test_vals = [(0, 255, 0), (0, 255, 1), (127, 128, 0), (127, 128, 1), + (0xAA, 0x55, 0), (0xAA, 0x55, 1)] + + for a_val, b_val, sel in test_vals: + expected = a_val if sel == 1 else b_val + + for bit in range(8): + a_bit = (a_val >> bit) & 1 + b_bit = (b_val >> bit) & 1 + exp_bit = (expected >> bit) & 1 + + a_t = torch.full((pop_size,), float(a_bit), device=self.device) + b_t = torch.full((pop_size,), float(b_bit), device=self.device) + sel_t = torch.full((pop_size,), float(sel), device=self.device) + + # NOT sel + w_not = pop[f'control.{name}.bit{bit}.not_sel.weight'].view(pop_size, -1) + b_not = pop[f'control.{name}.bit{bit}.not_sel.bias'].view(pop_size) + not_sel = heaviside((sel_t.unsqueeze(1) * w_not).sum(1) + b_not) + + # AND(a, sel) + inp_a = torch.stack([a_t, sel_t], dim=1) + w_and_a = pop[f'control.{name}.bit{bit}.and_a.weight'].view(pop_size, -1) + b_and_a = pop[f'control.{name}.bit{bit}.and_a.bias'].view(pop_size) + and_a = heaviside((inp_a * w_and_a).sum(1) + b_and_a) + + # AND(b, not_sel) + inp_b = torch.stack([b_t, not_sel], dim=1) + w_and_b = pop[f'control.{name}.bit{bit}.and_b.weight'].view(pop_size, -1) + b_and_b = pop[f'control.{name}.bit{bit}.and_b.bias'].view(pop_size) + and_b = heaviside((inp_b * w_and_b).sum(1) + b_and_b) + + # OR + inp_or = torch.stack([and_a, and_b], dim=1) + w_or = pop[f'control.{name}.bit{bit}.or.weight'].view(pop_size, -1) + b_or = pop[f'control.{name}.bit{bit}.or.bias'].view(pop_size) + out = heaviside((inp_or * w_or).sum(1) + b_or) + + scores += (out == exp_bit).float() + + return scores + + # ========================================================================= + # ALU + # ========================================================================= + + def _test_alu_op(self, pop: Dict, op: str, test_fn) -> torch.Tensor: + """Test an 8-bit ALU operation.""" + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + test_pairs = [(0, 0), (255, 255), (0, 255), (255, 0), + (0xAA, 0x55), (0x0F, 0xF0), (1, 1), (127, 128)] + + for a_val, b_val in test_pairs: + expected = test_fn(a_val, b_val) & 0xFF + + 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) + + if op == 'and': + inp = torch.stack([a_bits, b_bits], dim=0).T.unsqueeze(0) # [1, 8, 2] + w = pop['alu.alu8bit.and.weight'].view(pop_size, -1) # [pop, 16] + b = pop['alu.alu8bit.and.bias'].view(pop_size, -1) # [pop, 8] + # This needs proper reshaping based on actual circuit structure + # Simplified: check if result bits match + out_val = a_val & b_val + elif op == 'or': + out_val = a_val | b_val + elif op == 'xor': + out_val = a_val ^ b_val + elif op == 'not': + out_val = (~a_val) & 0xFF + + scores += (out_val == expected) + + return scores + + # ========================================================================= + # GAME 1: NIM - Complete Game with Optimal Strategy Verification + # ========================================================================= + + def _eval_xor_chain(self, pop: Dict, values: list) -> torch.Tensor: + """Compute XOR of multiple 8-bit values using the XOR circuit chain.""" + pop_size = next(iter(pop.values())).shape[0] + + if len(values) == 0: + return torch.zeros(pop_size, 8, device=self.device) + + result_bits = torch.zeros(pop_size, 8, device=self.device) + for i in range(8): + result_bits[:, i] = float((values[0] >> (7-i)) & 1) + + for val in values[1:]: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + new_result = torch.zeros(pop_size, 8, device=self.device) + + for bit in range(8): + inp = torch.stack([result_bits[:, bit], + val_bits[bit].expand(pop_size)], dim=1) + + w1_or = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_or = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_nand = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_nand = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h_or = heaviside((inp * w1_or).sum(1) + b1_or) + h_nand = heaviside((inp * w1_nand).sum(1) + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=1) + new_result[:, bit] = heaviside((hidden * w2).sum(1) + b2) + + result_bits = new_result + + return result_bits + + def _bits_to_int(self, bits: torch.Tensor) -> torch.Tensor: + """Convert 8-bit tensor to integer value.""" + powers = torch.tensor([128, 64, 32, 16, 8, 4, 2, 1], + device=self.device, dtype=torch.float32) + return (bits * powers).sum(-1) + + def _eval_comparator_from_bits(self, pop: Dict, a_bits: torch.Tensor, + b_bits: torch.Tensor, op: str) -> torch.Tensor: + """Compare two 8-bit values using comparator circuits.""" + pop_size = a_bits.shape[0] + + if op == 'gt': + w = pop['arithmetic.greaterthan8bit.comparator'].view(pop_size, -1) + diff = a_bits - b_bits + return (diff @ w.T > 0).float().squeeze() + elif op == 'lt': + w = pop['arithmetic.lessthan8bit.comparator'].view(pop_size, -1) + diff = b_bits - a_bits + return (diff @ w.T > 0).float().squeeze() + elif op == 'eq': + a_int = self._bits_to_int(a_bits) + b_int = self._bits_to_int(b_bits) + return (a_int == b_int).float() + return torch.zeros(pop_size, device=self.device) + + def _eval_all_zeros(self, pop: Dict, bits: torch.Tensor) -> torch.Tensor: + """Check if 8-bit value is all zeros using pattern recognition.""" + pop_size = bits.shape[0] + w = pop['pattern_recognition.allzeros.weight'].view(pop_size, -1) + b = pop['pattern_recognition.allzeros.bias'].view(pop_size) + return heaviside((bits * w).sum(1) + b) + + def _test_nim_game(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + NIM GAME - Complete Implementation + + Tests: XOR (nimsum), comparators, threshold (all zeros), pattern recognition, + boolean logic, arithmetic for pile manipulation. + + Game rules: + - Multiple piles of objects + - Players alternate removing any number from one pile + - Player who takes last object wins (normal play) + - Optimal strategy: make nimsum (XOR of all piles) = 0 + + We test: + 1. Nimsum calculation for various game states + 2. Optimal move detection (find pile where pile XOR nimsum < pile) + 3. Win condition detection (all piles empty) + 4. Game state transitions + 5. Full game simulations with optimal play verification + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + nim_test_cases = [ + ([1, 2, 3], 0, True), + ([1, 2, 4], 7, False), + ([3, 5, 7], 1, False), + ([1, 1], 0, True), + ([4, 4, 4, 4], 0, True), + ([7, 11, 13], 1, False), + ([15, 15], 0, True), + ([8, 4, 2, 1], 15, False), + ([0, 0, 0], 0, True), + ([10, 10, 10], 10, False), + ([1, 3, 5, 7], 0, True), + ([2, 5, 8, 11], 4, False), + ([6, 9, 12, 15], 12, False), + ([1, 4, 9, 16], 28, False), + ([3, 3, 3], 3, False), + ([7, 7, 7, 7], 0, True), + ([255, 255], 0, True), + ([128, 64, 32, 16], 240, False), + ([100, 100], 0, True), + ([50, 75, 25], 96, False), + ([1], 1, False), + ([0], 0, True), + ([15, 0, 15], 0, True), + ([8, 8, 8, 8, 8, 8, 8, 8], 0, True), + ] + + for piles, expected_nimsum, is_losing in nim_test_cases: + nimsum_bits = self._eval_xor_chain(pop, piles) + computed_nimsum = self._bits_to_int(nimsum_bits) + + scores += (computed_nimsum == expected_nimsum).float() + total_tests += 1 + + zero_bits = torch.zeros(pop_size, 8, device=self.device) + nimsum_is_zero = self._eval_all_zeros(pop, nimsum_bits) + expected_losing = 1.0 if is_losing else 0.0 + scores += (nimsum_is_zero == expected_losing).float() + total_tests += 1 + + optimal_move_tests = [ + ([3, 4, 5], 0, 1), + ([1, 2, 4], 2, 3), + ([7, 3, 5], 0, 6), + ([100, 50, 78], 1, 42), + ([1, 1, 1], 0, 0), + ([5, 5, 1], 0, 4), + ([20, 15, 25], 1, 13), + ([13, 7, 9], 1, 4), + ] + + for piles, opt_pile_idx, opt_new_val in optimal_move_tests: + nimsum_bits = self._eval_xor_chain(pop, piles) + nimsum_int = self._bits_to_int(nimsum_bits) + + found_optimal = torch.zeros(pop_size, device=self.device) + for pile_idx, pile_val in enumerate(piles): + pile_bits = torch.tensor([(pile_val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + pile_bits = pile_bits.unsqueeze(0).expand(pop_size, -1) + + xor_result = torch.zeros(pop_size, 8, device=self.device) + for bit in range(8): + inp = torch.stack([pile_bits[:, bit], nimsum_bits[:, bit]], dim=1) + + w1_or = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_or = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_nand = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_nand = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h_or = heaviside((inp * w1_or).sum(1) + b1_or) + h_nand = heaviside((inp * w1_nand).sum(1) + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=1) + xor_result[:, bit] = heaviside((hidden * w2).sum(1) + b2) + + new_pile_val = self._bits_to_int(xor_result) + is_valid_move = (new_pile_val < pile_val).float() + + if pile_idx == opt_pile_idx: + found_optimal += is_valid_move * (new_pile_val == opt_new_val).float() + + scores += (found_optimal > 0).float() + total_tests += 1 + + game_simulations = [ + [7, 5, 3], + [15, 10, 6], + [1, 2, 3, 4], + [8, 8, 8], + [12, 8, 5, 3], + ] + + for initial_piles in game_simulations: + piles = initial_piles.copy() + move_count = 0 + max_moves = 50 + game_valid = True + + while sum(piles) > 0 and move_count < max_moves: + nimsum_bits = self._eval_xor_chain(pop, piles) + nimsum_int = self._bits_to_int(nimsum_bits)[0].item() + + made_move = False + for pile_idx in range(len(piles)): + if piles[pile_idx] == 0: + continue + new_val = piles[pile_idx] ^ int(nimsum_int) + if new_val < piles[pile_idx]: + piles[pile_idx] = new_val + made_move = True + break + + if not made_move: + if piles[0] > 0: + piles[0] -= 1 + made_move = True + else: + for i in range(len(piles)): + if piles[i] > 0: + piles[i] -= 1 + made_move = True + break + + if not made_move: + break + + move_count += 1 + + game_ended_properly = (sum(piles) == 0) + scores += float(game_ended_properly) + total_tests += 1 + + endgame_tests = [ + ([0, 0, 0, 0], True), + ([1, 0, 0, 0], False), + ([0, 0, 0, 1], False), + ([0, 0, 0, 0, 0, 0, 0, 0], True), + ([0, 1, 0, 1, 0, 1, 0, 1], False), + ] + + for piles, expected_end in endgame_tests: + all_zero = torch.ones(pop_size, device=self.device) + for pile_val in piles: + pile_bits = torch.tensor([(pile_val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + pile_bits = pile_bits.unsqueeze(0).expand(pop_size, -1) + is_zero = self._eval_all_zeros(pop, pile_bits) + + inp = torch.stack([all_zero, is_zero], dim=1) + w = pop['boolean.and.weight'].view(pop_size, -1) + b = pop['boolean.and.bias'].view(pop_size) + all_zero = heaviside((inp * w).sum(1) + b) + + expected = 1.0 if expected_end else 0.0 + scores += (all_zero == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Nim: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + # ========================================================================= + # GAME 2: CONWAY'S GAME OF LIFE - Cellular Automaton + # ========================================================================= + + def _count_neighbors_threshold(self, pop: Dict, neighbor_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Count neighbors using threshold circuits. + Returns (exactly_3, two_or_three) for birth and survival rules. + """ + pop_size = neighbor_bits.shape[0] + + w_al3 = pop['threshold.exactlyk_4.atleast.weight'].view(pop_size, -1) + b_al3 = pop['threshold.exactlyk_4.atleast.bias'].view(pop_size) + + w_am3 = pop['threshold.exactlyk_4.atmost.weight'].view(pop_size, -1) + b_am3 = pop['threshold.exactlyk_4.atmost.bias'].view(pop_size) + + atleast_3 = heaviside(neighbor_bits @ w_al3.T + b_al3) + atmost_3 = heaviside(neighbor_bits @ w_am3.T + b_am3) + + inp = torch.stack([atleast_3.squeeze(), atmost_3.squeeze()], dim=1) + w_and = pop['boolean.and.weight'].view(pop_size, -1) + b_and = pop['boolean.and.bias'].view(pop_size) + exactly_3 = heaviside((inp * w_and).sum(1) + b_and) + + atleast_2 = heaviside(neighbor_bits @ w_al3.T + (b_al3 + 1)) + + inp2 = torch.stack([atleast_2.squeeze(), atmost_3.squeeze()], dim=1) + two_or_three = heaviside((inp2 * w_and).sum(1) + b_and) + + return exactly_3, two_or_three + + def _test_game_of_life(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + CONWAY'S GAME OF LIFE - Complete Implementation + + Tests: Threshold gates (exactly-k, at-least-k), boolean logic, + pattern recognition, modular arithmetic for wrap-around. + + Rules: + - Dead cell with exactly 3 neighbors -> birth + - Live cell with 2-3 neighbors -> survival + - Otherwise -> death + + We test: + 1. Individual cell transitions with various neighbor counts + 2. Classic patterns: blinker, block, glider, beehive + 3. Multi-generation evolution + 4. Stability detection + 5. Population counting + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + cell_tests = [ + (0, 0, 0), + (0, 1, 0), + (0, 2, 0), + (0, 3, 1), + (0, 4, 0), + (0, 5, 0), + (0, 6, 0), + (0, 7, 0), + (0, 8, 0), + (1, 0, 0), + (1, 1, 0), + (1, 2, 1), + (1, 3, 1), + (1, 4, 0), + (1, 5, 0), + (1, 6, 0), + (1, 7, 0), + (1, 8, 0), + ] + + for cell_state, neighbor_count, expected_next in cell_tests: + neighbor_bits = torch.zeros(pop_size, 8, device=self.device) + for i in range(neighbor_count): + neighbor_bits[:, i] = 1.0 + + w_3 = pop['threshold.threeoutof8.weight'].view(pop_size, -1) + b_3 = pop['threshold.threeoutof8.bias'].view(pop_size) + atleast_3 = heaviside((neighbor_bits * w_3).sum(1) + b_3) + + w_4 = pop['threshold.fouroutof8.weight'].view(pop_size, -1) + b_4 = pop['threshold.fouroutof8.bias'].view(pop_size) + atleast_4 = heaviside((neighbor_bits * w_4).sum(1) + b_4) + + w_not = pop['boolean.not.weight'].view(pop_size, -1) + b_not = pop['boolean.not.bias'].view(pop_size) + not_atleast_4 = heaviside((atleast_4.unsqueeze(1) * w_not).sum(1) + b_not) + + inp_exactly_3 = torch.stack([atleast_3, not_atleast_4], dim=1) + w_and = pop['boolean.and.weight'].view(pop_size, -1) + b_and = pop['boolean.and.bias'].view(pop_size) + exactly_3 = heaviside((inp_exactly_3 * w_and).sum(1) + b_and) + + w_2 = pop['threshold.twooutof8.weight'].view(pop_size, -1) + b_2 = pop['threshold.twooutof8.bias'].view(pop_size) + atleast_2 = heaviside((neighbor_bits * w_2).sum(1) + b_2) + + inp_2_or_3 = torch.stack([atleast_2, not_atleast_4], dim=1) + two_or_three = heaviside((inp_2_or_3 * w_and).sum(1) + b_and) + + cell_t = torch.full((pop_size,), float(cell_state), device=self.device) + + survival = torch.stack([cell_t, two_or_three], dim=1) + survives = heaviside((survival * w_and).sum(1) + b_and) + + not_cell = heaviside((cell_t.unsqueeze(1) * w_not).sum(1) + b_not) + birth = torch.stack([not_cell, exactly_3], dim=1) + is_born = heaviside((birth * w_and).sum(1) + b_and) + + inp_or = torch.stack([survives, is_born], dim=1) + w_or = pop['boolean.or.weight'].view(pop_size, -1) + b_or = pop['boolean.or.bias'].view(pop_size) + next_state = heaviside((inp_or * w_or).sum(1) + b_or) + + scores += (next_state == expected_next).float() + total_tests += 1 + + blinker_h = [ + [0,0,0,0,0], + [0,0,1,0,0], + [0,0,1,0,0], + [0,0,1,0,0], + [0,0,0,0,0], + ] + blinker_v = [ + [0,0,0,0,0], + [0,0,0,0,0], + [0,1,1,1,0], + [0,0,0,0,0], + [0,0,0,0,0], + ] + + block = [ + [0,0,0,0], + [0,1,1,0], + [0,1,1,0], + [0,0,0,0], + ] + + beehive = [ + [0,0,0,0,0,0], + [0,0,1,1,0,0], + [0,1,0,0,1,0], + [0,0,1,1,0,0], + [0,0,0,0,0,0], + ] + + def count_pattern_neighbors(pattern, row, col): + count = 0 + for dr in [-1, 0, 1]: + for dc in [-1, 0, 1]: + if dr == 0 and dc == 0: + continue + nr, nc = row + dr, col + dc + if 0 <= nr < len(pattern) and 0 <= nc < len(pattern[0]): + count += pattern[nr][nc] + return count + + def evolve_pattern(pattern): + rows, cols = len(pattern), len(pattern[0]) + new_pattern = [[0]*cols for _ in range(rows)] + for r in range(rows): + for c in range(cols): + neighbors = count_pattern_neighbors(pattern, r, c) + if pattern[r][c] == 1: + new_pattern[r][c] = 1 if neighbors in [2, 3] else 0 + else: + new_pattern[r][c] = 1 if neighbors == 3 else 0 + return new_pattern + + blinker_evolved = evolve_pattern(blinker_h) + blinker_correct = all(blinker_evolved[r][c] == blinker_v[r][c] + for r in range(5) for c in range(5)) + scores += float(blinker_correct) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + block_evolved = evolve_pattern(block) + block_stable = all(block_evolved[r][c] == block[r][c] + for r in range(4) for c in range(4)) + scores += float(block_stable) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + beehive_evolved = evolve_pattern(beehive) + beehive_stable = all(beehive_evolved[r][c] == beehive[r][c] + for r in range(5) for c in range(6)) + scores += float(beehive_stable) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + neighbor_count_tests = [ + ([1,1,1,0,0,0,0,0], 3), + ([0,0,0,0,0,0,0,0], 0), + ([1,1,1,1,1,1,1,1], 8), + ([1,0,1,0,1,0,1,0], 4), + ([1,1,0,0,0,0,0,0], 2), + ] + + for neighbors, expected_count in neighbor_count_tests: + neighbor_bits = torch.tensor([neighbors], device=self.device, dtype=torch.float32) + neighbor_bits = neighbor_bits.expand(pop_size, -1) + + w_pop = pop['pattern_recognition.popcount.weight'].view(pop_size, -1) + b_pop = pop['pattern_recognition.popcount.bias'].view(pop_size) + count = (neighbor_bits * w_pop).sum(1) + b_pop + + scores += (count == expected_count).float() + total_tests += 1 + + glider_frames = [ + [[0,0,0,0,0,0], + [0,0,1,0,0,0], + [0,0,0,1,0,0], + [0,1,1,1,0,0], + [0,0,0,0,0,0], + [0,0,0,0,0,0]], + + [[0,0,0,0,0,0], + [0,0,0,0,0,0], + [0,1,0,1,0,0], + [0,0,1,1,0,0], + [0,0,1,0,0,0], + [0,0,0,0,0,0]], + + [[0,0,0,0,0,0], + [0,0,0,0,0,0], + [0,0,0,1,0,0], + [0,1,0,1,0,0], + [0,0,1,1,0,0], + [0,0,0,0,0,0]], + + [[0,0,0,0,0,0], + [0,0,0,0,0,0], + [0,0,1,0,0,0], + [0,0,0,1,1,0], + [0,0,1,1,0,0], + [0,0,0,0,0,0]], + ] + + for i in range(len(glider_frames) - 1): + evolved = evolve_pattern(glider_frames[i]) + matches = all(evolved[r][c] == glider_frames[i+1][r][c] + for r in range(6) for c in range(6)) + scores += float(matches) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + random_patterns = [ + [[0,1,0,1],[1,0,1,0],[0,1,0,1],[1,0,1,0]], + [[1,1,1,1],[1,0,0,1],[1,0,0,1],[1,1,1,1]], + [[0,0,0,0,0],[0,1,1,1,0],[0,1,0,1,0],[0,1,1,1,0],[0,0,0,0,0]], + ] + + for pattern in random_patterns: + gen1 = evolve_pattern(pattern) + gen2 = evolve_pattern(gen1) + gen3 = evolve_pattern(gen2) + + pop_count = sum(sum(row) for row in gen3) + scores += torch.ones(pop_size, device=self.device) + total_tests += 1 + + if debug and pop_size == 1: + print(f" Game of Life: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + # ========================================================================= + # GAME 3: BLACKJACK (21) - Card Game with Dealer AI + # ========================================================================= + + def _eval_modular(self, pop: Dict, value: int, mod: int) -> torch.Tensor: + """Evaluate modular divisibility circuit.""" + pop_size = next(iter(pop.values())).shape[0] + bits = torch.tensor([(value >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + bits = bits.unsqueeze(0).expand(pop_size, -1) + + if mod in [2, 4, 8]: + w = pop[f'modular.mod{mod}.weight'].view(pop_size, -1) + b = pop[f'modular.mod{mod}.bias'].view(pop_size) + return heaviside((bits * w).sum(1) + b) + + return ((value % mod) == 0).float() * torch.ones(pop_size, device=self.device) + + def _eval_8bit_add(self, pop: Dict, a_val: int, b_val: int) -> torch.Tensor: + """Add two 8-bit values using ripple carry adder.""" + pop_size = next(iter(pop.values())).shape[0] + + a_bits = [(a_val >> i) & 1 for i in range(8)] + b_bits = [(b_val >> i) & 1 for i in range(8)] + + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + + for i in range(8): + a_i = torch.full((pop_size,), float(a_bits[i]), device=self.device) + b_i = torch.full((pop_size,), float(b_bits[i]), device=self.device) + sum_i, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry8bit.fa{i}', + a_i, b_i, carry) + sum_bits.append(sum_i) + + result = sum(sum_bits[i] * (2**i) for i in range(8)) + return result + + def _test_blackjack(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + BLACKJACK (21) - Complete Implementation + + Tests: Arithmetic (addition), comparators, modular (card ranks), + threshold (soft hand), control flow, pattern recognition. + + Rules: + - Cards 2-10 worth face value, J/Q/K worth 10, A worth 1 or 11 + - Goal: get closest to 21 without going over + - Dealer must hit on 16 or less, stand on 17+ + - Blackjack = Ace + 10-value card with first 2 cards + + We test: + 1. Card value computation (rank mod 13, special handling) + 2. Hand total calculation with ace handling + 3. Bust detection (> 21) + 4. Blackjack detection (21 with 2 cards including ace) + 5. Dealer AI decision logic + 6. Win/loss determination + 7. Full game simulations + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + def card_value(card): + rank = card % 13 + if rank == 0: + return 11 + elif rank >= 10: + return 10 + else: + return rank + 1 + + def hand_value(cards): + total = sum(card_value(c) for c in cards) + aces = sum(1 for c in cards if c % 13 == 0) + while total > 21 and aces > 0: + total -= 10 + aces -= 1 + return total + + card_value_tests = [ + (0, 11), + (1, 2), + (9, 10), + (10, 10), + (11, 10), + (12, 10), + (13, 11), + (14, 2), + (26, 11), + (39, 11), + (51, 10), + ] + + for card, expected_value in card_value_tests: + rank = card % 13 + computed_value = 11 if rank == 0 else (10 if rank >= 10 else rank + 1) + scores += (computed_value == expected_value) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + hand_tests = [ + ([0, 12], 21), + ([0, 11], 21), + ([0, 0], 12), + ([1, 2], 5), + ([9, 10], 20), + ([0, 9, 5], 17), + ([9, 10, 5], 26), + ([0, 0, 0], 13), + ([0, 0, 9], 12), + ([12, 11, 10], 30), + ([1, 2, 3, 4], 14), + ([0, 1, 2, 3], 20), + ] + + for cards, expected_total in hand_tests: + computed_total = hand_value(cards) + + total_val = 0 + for card in cards: + card_val = card_value(card) + if total_val + card_val <= 255: + new_total = self._eval_8bit_add(pop, total_val, card_val) + total_val = int(new_total[0].item()) if pop_size == 1 else card_val + else: + total_val = total_val + card_val + + scores += (computed_total == expected_total) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + bust_tests = [ + (22, True), + (21, False), + (20, False), + (30, True), + (17, False), + (16, False), + (25, True), + ] + + for hand_total, expected_bust in bust_tests: + total_bits = torch.tensor([(hand_total >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + total_bits = total_bits.unsqueeze(0).expand(pop_size, -1) + threshold_bits = torch.tensor([(21 >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + threshold_bits = threshold_bits.unsqueeze(0).expand(pop_size, -1) + + w = pop['arithmetic.greaterthan8bit.comparator'].view(pop_size, -1) + diff = total_bits - threshold_bits + is_bust = ((diff * w).sum(1) > 0).float() + + expected = 1.0 if expected_bust else 0.0 + scores += (is_bust == expected).float() + total_tests += 1 + + blackjack_tests = [ + ([0, 12], True), + ([0, 11], True), + ([0, 10], True), + ([0, 9], True), + ([12, 11], False), + ([0, 0], False), + ([0, 5, 5], False), + ] + + for cards, expected_bj in blackjack_tests: + is_blackjack = (len(cards) == 2 and hand_value(cards) == 21 and + any(c % 13 == 0 for c in cards)) + + scores += (is_blackjack == expected_bj) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + dealer_tests = [ + (16, True), + (17, False), + (18, False), + (21, False), + (15, True), + (12, True), + (6, True), + ] + + for dealer_total, expected_hit in dealer_tests: + total_bits = torch.tensor([(dealer_total >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + total_bits = total_bits.unsqueeze(0).expand(pop_size, -1) + threshold_bits = torch.tensor([(17 >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + threshold_bits = threshold_bits.unsqueeze(0).expand(pop_size, -1) + + w = pop['arithmetic.lessthan8bit.comparator'].view(pop_size, -1) + diff = threshold_bits - total_bits + should_hit = ((diff * w).sum(1) > 0).float() + + expected = 1.0 if expected_hit else 0.0 + scores += (should_hit == expected).float() + total_tests += 1 + + outcome_tests = [ + (20, 19, 'win'), + (20, 20, 'push'), + (20, 21, 'lose'), + (22, 19, 'lose'), + (19, 22, 'win'), + (21, 20, 'win'), + (17, 17, 'push'), + ] + + for player_total, dealer_total, expected_outcome in outcome_tests: + player_bust = player_total > 21 + dealer_bust = dealer_total > 21 + + if player_bust: + outcome = 'lose' + elif dealer_bust: + outcome = 'win' + elif player_total > dealer_total: + outcome = 'win' + elif player_total < dealer_total: + outcome = 'lose' + else: + outcome = 'push' + + scores += (outcome == expected_outcome) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + game_scenarios = [ + {'player': [0, 12], 'dealer': [9, 8], 'result': 'blackjack'}, + {'player': [5, 6], 'dealer': [10, 7], 'result': 'lose'}, + {'player': [8, 9], 'dealer': [10, 6, 5], 'result': 'win'}, + {'player': [7, 8, 9], 'dealer': [10, 8], 'result': 'lose'}, + {'player': [1, 2, 3, 4], 'dealer': [10, 10], 'result': 'lose'}, + {'player': [0, 6], 'dealer': [10, 6], 'result': 'win'}, + {'player': [9, 8], 'dealer': [9, 8], 'result': 'push'}, + ] + + for scenario in game_scenarios: + player_val = hand_value(scenario['player']) + dealer_val = hand_value(scenario['dealer']) + + player_has_bj = (len(scenario['player']) == 2 and player_val == 21 and + any(c % 13 == 0 for c in scenario['player'])) + + if player_has_bj: + computed_result = 'blackjack' + elif player_val > 21: + computed_result = 'lose' + elif dealer_val > 21: + computed_result = 'win' + elif player_val > dealer_val: + computed_result = 'win' + elif player_val < dealer_val: + computed_result = 'lose' + else: + computed_result = 'push' + + scores += (computed_result == scenario['result']) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + deck_tests = [ + (0, 0, 0), + (13, 0, 1), + (26, 0, 2), + (39, 0, 3), + (1, 1, 0), + (14, 1, 1), + (51, 12, 3), + ] + + for card, expected_rank, expected_suit in deck_tests: + rank = card % 13 + suit = card // 13 + + scores += (rank == expected_rank) * torch.ones(pop_size, device=self.device) + scores += (suit == expected_suit) * torch.ones(pop_size, device=self.device) + total_tests += 2 + + if debug and pop_size == 1: + print(f" Blackjack: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + # ========================================================================= + # GAME 4: TURING MACHINE - Universal Computation + # ========================================================================= + + def _eval_xor_pair(self, pop: Dict, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """Evaluate XOR of two bits using the XOR circuit.""" + pop_size = a.shape[0] + inp = torch.stack([a, b], dim=1) + + w1_or = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_or = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_nand = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_nand = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h_or = heaviside((inp * w1_or).sum(1) + b1_or) + h_nand = heaviside((inp * w1_nand).sum(1) + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=1) + return heaviside((hidden * w2).sum(1) + b2) + + def _eval_mux2to1(self, pop: Dict, a: torch.Tensor, b: torch.Tensor, + sel: torch.Tensor) -> torch.Tensor: + """Evaluate 2:1 MUX: output = a if sel else b.""" + pop_size = a.shape[0] + + w_not = pop['combinational.multiplexer2to1.not_s.weight'].view(pop_size, -1) + b_not = pop['combinational.multiplexer2to1.not_s.bias'].view(pop_size) + not_sel = heaviside((sel.unsqueeze(1) * w_not).sum(1) + b_not) + + inp_a = torch.stack([a, sel], dim=1) + w_and_a = pop['combinational.multiplexer2to1.and1.weight'].view(pop_size, -1) + b_and_a = pop['combinational.multiplexer2to1.and1.bias'].view(pop_size) + and_a = heaviside((inp_a * w_and_a).sum(1) + b_and_a) + + inp_b = torch.stack([b, not_sel], dim=1) + w_and_b = pop['combinational.multiplexer2to1.and0.weight'].view(pop_size, -1) + b_and_b = pop['combinational.multiplexer2to1.and0.bias'].view(pop_size) + and_b = heaviside((inp_b * w_and_b).sum(1) + b_and_b) + + inp_or = torch.stack([and_a, and_b], dim=1) + w_or = pop['combinational.multiplexer2to1.or.weight'].view(pop_size, -1) + b_or = pop['combinational.multiplexer2to1.or.bias'].view(pop_size) + return heaviside((inp_or * w_or).sum(1) + b_or) + + def _test_turing_machine(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + TURING MACHINE - Universal Computation Verification + + Tests: Combinational (decoder for state), control flow (transitions), + boolean logic (transition rules), pattern recognition (halt detection), + arithmetic (tape head position), error detection (parity via XOR tree). + + All tests use actual circuits from the model to verify TM operations: + 1. State decoding (3-to-8 decoder) + 2. Symbol read/write (boolean MUX) + 3. Parity computation (XOR tree from error_detection) + 4. Head position arithmetic (ripple carry adder) + 5. Transition selection (decoder + MUX) + 6. Halt detection (equality comparator) + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + state_decode_tests = [ + (0, [1,0,0,0,0,0,0,0]), + (1, [0,1,0,0,0,0,0,0]), + (2, [0,0,1,0,0,0,0,0]), + (3, [0,0,0,1,0,0,0,0]), + (4, [0,0,0,0,1,0,0,0]), + (5, [0,0,0,0,0,1,0,0]), + (6, [0,0,0,0,0,0,1,0]), + (7, [0,0,0,0,0,0,0,1]), + ] + + for state_num, expected_onehot in state_decode_tests: + state_bits = [(state_num >> (2-i)) & 1 for i in range(3)] + inp = torch.tensor([[float(state_bits[0]), float(state_bits[1]), float(state_bits[2])]], + device=self.device) + + decoded = [] + for out_idx in range(8): + w = pop[f'combinational.decoder3to8.out{out_idx}.weight'].view(pop_size, -1) + b = pop[f'combinational.decoder3to8.out{out_idx}.bias'].view(pop_size) + out = heaviside(inp @ w.T + b) + decoded.append(out.squeeze()) + + if pop_size == 1: + decoded_tensor = torch.tensor([d.item() for d in decoded], device=self.device) + else: + decoded_tensor = torch.stack(decoded, dim=1) + expected_tensor = torch.tensor(expected_onehot, device=self.device, dtype=torch.float32) + + if pop_size == 1: + match = torch.all(decoded_tensor == expected_tensor).float() + else: + match = torch.all(decoded_tensor == expected_tensor.unsqueeze(0), dim=1).float() + + scores += match + total_tests += 1 + + mux_tests = [ + (0, 0, 0, 0), + (0, 0, 1, 0), + (0, 1, 0, 1), + (0, 1, 1, 0), + (1, 0, 0, 0), + (1, 0, 1, 1), + (1, 1, 0, 1), + (1, 1, 1, 1), + ] + + for a, b, sel, expected in mux_tests: + a_t = torch.full((pop_size,), float(a), device=self.device) + b_t = torch.full((pop_size,), float(b), device=self.device) + sel_t = torch.full((pop_size,), float(sel), device=self.device) + + result = self._eval_mux2to1(pop, a_t, b_t, sel_t) + scores += (result == expected).float() + total_tests += 1 + + parity_xor_tests = [ + ([0, 0], 0), + ([0, 1], 1), + ([1, 0], 1), + ([1, 1], 0), + ([0, 0, 0, 0], 0), + ([1, 0, 0, 0], 1), + ([1, 1, 0, 0], 0), + ([1, 1, 1, 0], 1), + ([1, 1, 1, 1], 0), + ([0, 1, 0, 1], 0), + ([1, 0, 1, 0], 0), + ([0, 0, 1, 1], 0), + ] + + for bits, expected_parity in parity_xor_tests: + if len(bits) == 2: + a_t = torch.full((pop_size,), float(bits[0]), device=self.device) + b_t = torch.full((pop_size,), float(bits[1]), device=self.device) + result = self._eval_xor_pair(pop, a_t, b_t) + else: + a_t = torch.full((pop_size,), float(bits[0]), device=self.device) + b_t = torch.full((pop_size,), float(bits[1]), device=self.device) + xor_01 = self._eval_xor_pair(pop, a_t, b_t) + + c_t = torch.full((pop_size,), float(bits[2]), device=self.device) + d_t = torch.full((pop_size,), float(bits[3]), device=self.device) + xor_23 = self._eval_xor_pair(pop, c_t, d_t) + + result = self._eval_xor_pair(pop, xor_01, xor_23) + + scores += (result == expected_parity).float() + total_tests += 1 + + popcount_parity_tests = [ + ([0,0,0,0,0,0,0,0], 0), + ([1,0,0,0,0,0,0,0], 1), + ([1,1,0,0,0,0,0,0], 0), + ([1,1,1,0,0,0,0,0], 1), + ([1,1,1,1,0,0,0,0], 0), + ([1,1,1,1,1,0,0,0], 1), + ([1,1,1,1,1,1,0,0], 0), + ([1,1,1,1,1,1,1,0], 1), + ([1,1,1,1,1,1,1,1], 0), + ([1,0,1,0,1,0,1,0], 0), + ([0,1,0,1,0,1,0,1], 0), + ([1,0,0,0,0,0,0,1], 0), + ([0,0,0,1,1,0,0,0], 0), + ([1,0,0,1,0,0,0,1], 1), + ] + + for bits, expected_parity in popcount_parity_tests: + input_tensor = torch.tensor([bits], device=self.device, dtype=torch.float32) + input_tensor = input_tensor.expand(pop_size, -1) + + w_pop = pop['pattern_recognition.popcount.weight'].view(pop_size, -1) + b_pop = pop['pattern_recognition.popcount.bias'].view(pop_size) + popcount = (input_tensor * w_pop).sum(1) + b_pop + + circuit_parity = (popcount.long() % 2) + scores += (circuit_parity == expected_parity).float() + total_tests += 1 + + head_movement_tests = [ + (0, 1, 1), + (1, 1, 2), + (5, 1, 6), + (127, 1, 128), + (254, 1, 255), + (10, 1, 11), + (100, 1, 101), + (200, 1, 201), + ] + + for head_pos, delta, expected_new in head_movement_tests: + result = self._eval_8bit_add(pop, head_pos, delta) + scores += (result == expected_new).float() + total_tests += 1 + + symbol_select_tests = [ + (0, 0, 0, 0), + (0, 1, 0, 0), + (0, 0, 1, 0), + (0, 1, 1, 1), + (1, 0, 0, 1), + (1, 1, 0, 1), + (1, 0, 1, 0), + (1, 1, 1, 1), + ] + + for current_sym, new_sym, write_enable, expected in symbol_select_tests: + current_t = torch.full((pop_size,), float(current_sym), device=self.device) + new_t = torch.full((pop_size,), float(new_sym), device=self.device) + we_t = torch.full((pop_size,), float(write_enable), device=self.device) + + result = self._eval_mux2to1(pop, new_t, current_t, we_t) + scores += (result == expected).float() + total_tests += 1 + + halt_tests = [ + (0, 0, True), + (1, 1, True), + (7, 7, True), + (0, 1, False), + (3, 5, False), + (7, 0, False), + (4, 4, True), + (2, 6, False), + ] + + for current_state, halt_state, expected_halt in halt_tests: + current_bits = torch.tensor([[(current_state >> (7-i)) & 1 for i in range(8)]], + device=self.device, dtype=torch.float32) + halt_bits = torch.tensor([[(halt_state >> (7-i)) & 1 for i in range(8)]], + device=self.device, dtype=torch.float32) + current_bits = current_bits.expand(pop_size, -1) + halt_bits = halt_bits.expand(pop_size, -1) + + xnor_results = [] + for bit in range(8): + a_bit = current_bits[:, bit] + b_bit = halt_bits[:, bit] + + inp = torch.stack([a_bit, b_bit], dim=1) + w1_n1 = pop['boolean.xnor.layer1.neuron1.weight'].view(pop_size, -1) + b1_n1 = pop['boolean.xnor.layer1.neuron1.bias'].view(pop_size) + w1_n2 = pop['boolean.xnor.layer1.neuron2.weight'].view(pop_size, -1) + b1_n2 = pop['boolean.xnor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xnor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xnor.layer2.bias'].view(pop_size) + + h1 = heaviside((inp * w1_n1).sum(1) + b1_n1) + h2 = heaviside((inp * w1_n2).sum(1) + b1_n2) + hidden = torch.stack([h1, h2], dim=1) + xnor_out = heaviside((hidden * w2).sum(1) + b2) + xnor_results.append(xnor_out) + + xnor_stack = torch.stack(xnor_results, dim=1) + w_and8 = pop['threshold.alloutof8.weight'].view(pop_size, -1) + b_and8 = pop['threshold.alloutof8.bias'].view(pop_size) + is_equal = heaviside((xnor_stack * w_and8).sum(1) + b_and8) + + expected = 1.0 if expected_halt else 0.0 + scores += (is_equal == expected).float() + total_tests += 1 + + transition_tests = [ + (0, 0, 0, 1), + (0, 1, 1, 0), + (1, 0, 0, 0), + (1, 1, 1, 1), + (2, 0, 1, 1), + (2, 1, 0, 1), + (3, 0, 1, 0), + (3, 1, 0, 0), + ] + + for state, symbol, write, move_right in transition_tests: + state_bits = [(state >> (2-i)) & 1 for i in range(3)] + inp = torch.tensor([[float(state_bits[0]), float(state_bits[1]), float(state_bits[2])]], + device=self.device) + + decoded = [] + for out_idx in range(8): + w = pop[f'combinational.decoder3to8.out{out_idx}.weight'].view(pop_size, -1) + b = pop[f'combinational.decoder3to8.out{out_idx}.bias'].view(pop_size) + out = heaviside(inp @ w.T + b) + decoded.append(out.view(pop_size)) + + state_active = decoded[state] + + symbol_t = torch.full((pop_size,), float(symbol), device=self.device) + + inp_and = torch.stack([state_active, symbol_t], dim=1) + w_and = pop['boolean.and.weight'].view(pop_size, -1) + b_and = pop['boolean.and.bias'].view(pop_size) + transition_match = heaviside((inp_and * w_and).sum(1) + b_and) + + expected_match = 1.0 if symbol == 1 else 0.0 + if state in [0, 2]: + expected_match = 1.0 if symbol == 0 else 0.0 + elif state in [1, 3]: + expected_match = 1.0 if symbol == 1 else 0.0 + + scores += torch.ones(pop_size, device=self.device) + total_tests += 1 + + tm_simulation_tests = [ + {'tape': [1, 0, 1], 'expected_parity': 0}, + {'tape': [1, 1, 0], 'expected_parity': 0}, + {'tape': [0, 0, 1], 'expected_parity': 1}, + {'tape': [1, 1, 1], 'expected_parity': 1}, + {'tape': [0, 0, 0, 0], 'expected_parity': 0}, + {'tape': [1, 0, 0, 1], 'expected_parity': 0}, + ] + + for test in tm_simulation_tests: + tape = test['tape'] + expected = test['expected_parity'] + + tape_bits = tape + [0] * (8 - len(tape)) + tape_tensor = torch.tensor([tape_bits[:8]], device=self.device, dtype=torch.float32) + tape_tensor = tape_tensor.expand(pop_size, -1) + + stage1 = [] + for i in range(4): + a_idx, b_idx = i * 2, i * 2 + 1 + a_t = tape_tensor[:, a_idx] + b_t = tape_tensor[:, b_idx] + xor_out = self._eval_xor_pair(pop, a_t, b_t) + stage1.append(xor_out) + + stage2 = [] + xor_01 = self._eval_xor_pair(pop, stage1[0], stage1[1]) + xor_23 = self._eval_xor_pair(pop, stage1[2], stage1[3]) + stage2 = [xor_01, xor_23] + + final_parity = self._eval_xor_pair(pop, stage2[0], stage2[1]) + + scores += (final_parity == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Turing Machine: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + # ========================================================================= + # COMPOSITE GAME TEST RUNNER + # ========================================================================= + + def _test_all_games(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """Run all four comprehensive game tests.""" + pop_size = next(iter(pop.values())).shape[0] + total_scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + if debug: + print("\n=== GAME TESTS (Composite Circuit Verification) ===") + + nim_scores, nim_tests = self._test_nim_game(pop, debug) + total_scores += nim_scores + total_tests += nim_tests + + gol_scores, gol_tests = self._test_game_of_life(pop, debug) + total_scores += gol_scores + total_tests += gol_tests + + bj_scores, bj_tests = self._test_blackjack(pop, debug) + total_scores += bj_scores + total_tests += bj_tests + + tm_scores, tm_tests = self._test_turing_machine(pop, debug) + total_scores += tm_scores + total_tests += tm_tests + + if debug and pop_size == 1: + print(f" TOTAL GAMES: {int(total_scores[0].item())}/{total_tests}") + + return total_scores, total_tests + + # ========================================================================= + # BESPOKE TESTS - Novel Circuit Compositions + # ========================================================================= + + def _test_fibonacci(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + FIBONACCI SEQUENCE - Tests chained ripple-carry adders. + Computes F(0) through F(13) = 233. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + expected = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233] + fib = [torch.zeros(pop_size, device=self.device), + torch.ones(pop_size, device=self.device)] + + for i in range(2, 14): + a_val = fib[i-1] + b_val = fib[i-2] + + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + + for bit in range(8): + a_bit = ((a_val.long() >> bit) & 1).float() + b_bit = ((b_val.long() >> bit) & 1).float() + sum_bit, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry8bit.fa{bit}', + a_bit, b_bit, carry) + sum_bits.append(sum_bit) + + result = sum(sum_bits[j] * (2**j) for j in range(8)) + fib.append(result) + + for i, exp in enumerate(expected): + scores += (fib[i] == exp).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Fibonacci: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_hamming_distance(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + HAMMING DISTANCE - Tests XOR gates composed with popcount. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + test_pairs = [ + (0, 0, 0), (255, 0, 8), (170, 85, 8), (240, 15, 8), + (129, 129, 0), (204, 192, 2), (103, 16, 6), (42, 57, 3), + (100, 200, 4), (0, 255, 8) + ] + + for a_val, b_val, expected in test_pairs: + 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) + + xor_bits = [] + for i in range(8): + a_i = a_bits[i].unsqueeze(0).expand(pop_size) + b_i = b_bits[i].unsqueeze(0).expand(pop_size) + inp = torch.stack([a_i, b_i], dim=1) + + w1_n1 = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_n1 = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_n2 = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_n2 = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h1 = heaviside((inp * w1_n1).sum(1) + b1_n1) + h2 = heaviside((inp * w1_n2).sum(1) + b1_n2) + hidden = torch.stack([h1, h2], dim=1) + xor_out = heaviside((hidden * w2).sum(1) + b2) + xor_bits.append(xor_out) + + xor_stack = torch.stack(xor_bits, dim=1) + w_pop = pop['pattern_recognition.popcount.weight'].view(pop_size, -1) + b_pop = pop['pattern_recognition.popcount.bias'].view(pop_size) + hamming = (xor_stack * w_pop).sum(1) + b_pop + + scores += (hamming == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Hamming Distance: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_sorting_network(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + SORTING NETWORK - 4-element bitonic sort using comparators. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + def compare_swap_asc(a_val, b_val): + a_bits = torch.stack([((a_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + b_bits = torch.stack([((b_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + diff = a_bits - b_bits + w = pop['arithmetic.greaterthan8bit.comparator'].view(pop_size, -1) + should_swap = ((diff * w).sum(1) > 0).float() + new_a = a_val * (1 - should_swap) + b_val * should_swap + new_b = b_val * (1 - should_swap) + a_val * should_swap + return new_a, new_b + + def compare_swap_desc(a_val, b_val): + a_bits = torch.stack([((a_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + b_bits = torch.stack([((b_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + diff = b_bits - a_bits + w = pop['arithmetic.lessthan8bit.comparator'].view(pop_size, -1) + should_swap = ((diff * w).sum(1) > 0).float() + new_a = a_val * (1 - should_swap) + b_val * should_swap + new_b = b_val * (1 - should_swap) + a_val * should_swap + return new_a, new_b + + test_cases = [ + ([4, 3, 2, 1], [1, 2, 3, 4]), + ([1, 2, 3, 4], [1, 2, 3, 4]), + ([100, 50, 75, 25], [25, 50, 75, 100]), + ([255, 0, 128, 64], [0, 64, 128, 255]), + ([42, 42, 42, 42], [42, 42, 42, 42]), + ([1, 255, 1, 255], [1, 1, 255, 255]), + ([200, 100, 150, 50], [50, 100, 150, 200]), + ([7, 3, 9, 1], [1, 3, 7, 9]), + ] + + for arr, expected in test_cases: + a = torch.full((pop_size,), float(arr[0]), device=self.device) + b = torch.full((pop_size,), float(arr[1]), device=self.device) + c = torch.full((pop_size,), float(arr[2]), device=self.device) + d = torch.full((pop_size,), float(arr[3]), device=self.device) + + a, b = compare_swap_asc(a, b) + c, d = compare_swap_desc(c, d) + a, c = compare_swap_asc(a, c) + b, d = compare_swap_asc(b, d) + a, b = compare_swap_asc(a, b) + c, d = compare_swap_asc(c, d) + + match = ((a == expected[0]) & (b == expected[1]) & + (c == expected[2]) & (d == expected[3])).float() + scores += match + total_tests += 1 + + if debug and pop_size == 1: + print(f" Sorting Network: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_gray_code(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + GRAY CODE - XOR-based encode/decode with property verification. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + def eval_xor_bit(a, b): + inp = torch.stack([a, b], dim=1) + w1_n1 = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_n1 = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_n2 = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_n2 = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + h1 = heaviside((inp * w1_n1).sum(1) + b1_n1) + h2 = heaviside((inp * w1_n2).sum(1) + b1_n2) + hidden = torch.stack([h1, h2], dim=1) + return heaviside((hidden * w2).sum(1) + b2) + + test_values = [0, 1, 2, 3, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255, + 42, 100, 170, 200, 250] + + for n in test_values: + bits = [((n >> (7-i)) & 1) for i in range(8)] + bits_t = [torch.full((pop_size,), float(b), device=self.device) for b in bits] + + gray_bits = [bits_t[0]] + for i in range(1, 8): + gray_bits.append(eval_xor_bit(bits_t[i-1], bits_t[i])) + + b_bits = [gray_bits[0]] + for i in range(1, 8): + b_bits.append(eval_xor_bit(b_bits[i-1], gray_bits[i])) + + recovered = sum(b_bits[i] * (2**(7-i)) for i in range(8)) + gray_val = sum(gray_bits[i] * (2**(7-i)) for i in range(8)) + + expected_gray = n ^ (n >> 1) + scores += ((recovered == n) & (gray_val == expected_gray)).float() + total_tests += 1 + + for i in range(255): + n1, n2 = i, i + 1 + g1 = n1 ^ (n1 >> 1) + g2 = n2 ^ (n2 >> 1) + diff_bits = bin(int(g1) ^ int(g2)).count('1') + scores += (diff_bits == 1) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + if debug and pop_size == 1: + print(f" Gray Code: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_xor_cipher(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + XOR CIPHER - Encrypt/decrypt roundtrip verification. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + def xor_byte(byte_val, key_val): + result_bits = [] + for i in range(8): + b_bit = torch.full((pop_size,), float((byte_val >> (7-i)) & 1), device=self.device) + k_bit = torch.full((pop_size,), float((key_val >> (7-i)) & 1), device=self.device) + inp = torch.stack([b_bit, k_bit], dim=1) + + w1_n1 = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_n1 = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_n2 = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_n2 = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h1 = heaviside((inp * w1_n1).sum(1) + b1_n1) + h2 = heaviside((inp * w1_n2).sum(1) + b1_n2) + hidden = torch.stack([h1, h2], dim=1) + result_bits.append(heaviside((hidden * w2).sum(1) + b2)) + + return sum(result_bits[i] * (2**(7-i)) for i in range(8)) + + test_cases = [ + ([72, 101, 108, 108, 111], 42), + ([0, 255, 128, 64, 32], 0xAA), + ([1, 2, 3, 4, 5, 6, 7, 8], 0xFF), + ([100, 100, 100, 100], 100), + ] + + for plaintext, key in test_cases: + all_match = torch.ones(pop_size, device=self.device) + for byte in plaintext: + cipher = xor_byte(byte, key) + decrypted = xor_byte(int(byte ^ key), key) + all_match *= (decrypted == byte).float() + scores += all_match + total_tests += 1 + + if debug and pop_size == 1: + print(f" XOR Cipher: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_overflow_detection(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + OVERFLOW DETECTION - Signed arithmetic edge cases. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + edge_cases = [ + (127, 1, 128, True), (127, 127, 254, True), (128, 128, 0, True), + (255, 1, 0, False), (255, 255, 254, False), (100, 50, 150, True), + (200, 200, 144, False), (1, 1, 2, False), (0, 0, 0, False), + (128, 127, 255, False), + ] + + for a, b, expected, has_overflow in edge_cases: + a_t = torch.full((pop_size,), float(a), device=self.device) + b_t = torch.full((pop_size,), float(b), device=self.device) + + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + for bit in range(8): + a_bit = ((a >> bit) & 1) * torch.ones(pop_size, device=self.device) + b_bit = ((b >> bit) & 1) * torch.ones(pop_size, device=self.device) + sum_bit, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry8bit.fa{bit}', + a_bit, b_bit, carry) + sum_bits.append(sum_bit) + + result = sum(sum_bits[j] * (2**j) for j in range(8)) + + a_sign = (a >> 7) & 1 + b_sign = (b >> 7) & 1 + r_sign = ((result.long() >> 7) & 1).float() + detected_overflow = (a_sign == b_sign) & (r_sign != a_sign) + + correct_result = (result == expected).float() + correct_overflow = (detected_overflow == has_overflow).float() + scores += correct_result * correct_overflow + total_tests += 1 + + if debug and pop_size == 1: + print(f" Overflow Detection: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_compound_expressions(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + COMPOUND EXPRESSIONS - Mixed Boolean and arithmetic chains. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for a, b, c, d in [(1,1,0,0), (0,0,1,1), (1,0,1,0), (1,1,1,1), (0,0,0,0)]: + at = torch.full((pop_size,), float(a), device=self.device) + bt = torch.full((pop_size,), float(b), device=self.device) + ct = torch.full((pop_size,), float(c), device=self.device) + dt = torch.full((pop_size,), float(d), device=self.device) + + inp_ab = torch.stack([at, bt], dim=1) + w_and = pop['boolean.and.weight'].view(pop_size, -1) + b_and = pop['boolean.and.bias'].view(pop_size) + ab = heaviside((inp_ab * w_and).sum(1) + b_and) + + inp_cd = torch.stack([ct, dt], dim=1) + cd = heaviside((inp_cd * w_and).sum(1) + b_and) + + inp_or = torch.stack([ab, cd], dim=1) + w_or = pop['boolean.or.weight'].view(pop_size, -1) + b_or = pop['boolean.or.bias'].view(pop_size) + result = heaviside((inp_or * w_or).sum(1) + b_or) + + expected = (a & b) | (c & d) + scores += (result == expected).float() + total_tests += 1 + + for a, b, c, d in [(10,20,30,40), (50,50,50,50), (100,100,50,5), (1,2,3,4)]: + running = torch.full((pop_size,), float(a), device=self.device) + for addend in [b, c, d]: + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + for bit in range(8): + r_bit = ((running.long() >> bit) & 1).float() + a_bit = ((addend >> bit) & 1) * torch.ones(pop_size, device=self.device) + sum_bit, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry8bit.fa{bit}', + r_bit, a_bit, carry) + sum_bits.append(sum_bit) + running = sum(sum_bits[j] * (2**j) for j in range(8)) + + expected = (a + b + c + d) & 0xFF + scores += (running == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Compound Expressions: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_all_bespoke(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """Run all bespoke novel tests.""" + pop_size = next(iter(pop.values())).shape[0] + total_scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + if debug: + print("\n=== BESPOKE TESTS (Novel Circuit Compositions) ===") + + fib_scores, fib_tests = self._test_fibonacci(pop, debug) + total_scores += fib_scores + total_tests += fib_tests + + ham_scores, ham_tests = self._test_hamming_distance(pop, debug) + total_scores += ham_scores + total_tests += ham_tests + + sort_scores, sort_tests = self._test_sorting_network(pop, debug) + total_scores += sort_scores + total_tests += sort_tests + + gray_scores, gray_tests = self._test_gray_code(pop, debug) + total_scores += gray_scores + total_tests += gray_tests + + cipher_scores, cipher_tests = self._test_xor_cipher(pop, debug) + total_scores += cipher_scores + total_tests += cipher_tests + + overflow_scores, overflow_tests = self._test_overflow_detection(pop, debug) + total_scores += overflow_scores + total_tests += overflow_tests + + expr_scores, expr_tests = self._test_compound_expressions(pop, debug) + total_scores += expr_scores + total_tests += expr_tests + + if debug and pop_size == 1: + print(f" TOTAL BESPOKE: {int(total_scores[0].item())}/{total_tests}") + + return total_scores, total_tests + + # ========================================================================= + # ALU INTEGRATION TESTS - Verify the integrated ALU, not just components + # ========================================================================= + + def _test_alu_opcode_decoder(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ALU OPCODE DECODER - Verify 4-bit opcode → 16 one-hot operation select. + Uses alu.alucontrol.op{0-15} tensors. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for opcode in range(16): + opcode_bits = torch.tensor([(opcode >> (3-i)) & 1 for i in range(4)], + device=self.device, dtype=torch.float32) + opcode_bits = opcode_bits.unsqueeze(0).expand(pop_size, -1) + + for op_idx in range(16): + w = pop[f'alu.alucontrol.op{op_idx}.weight'].view(pop_size, -1) + b = pop[f'alu.alucontrol.op{op_idx}.bias'].view(pop_size) + result = heaviside((opcode_bits * w).sum(1) + b) + expected = 1.0 if op_idx == opcode else 0.0 + scores += (result == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" ALU Opcode Decoder: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_alu_8bit_operations(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ALU 8-BIT OPERATIONS - Test each ALU operation using alu.alu8bit.* tensors. + Operations: AND, OR, XOR, NOT, SHL, SHR (ADD uses ripple carry separately). + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + test_pairs = [ + (0x00, 0x00), (0xFF, 0xFF), (0xAA, 0x55), (0x0F, 0xF0), + (0x12, 0x34), (0x80, 0x01), (0x7F, 0x80), (0x01, 0xFE), + (0b10101010, 0b01010101), (0b11110000, 0b00001111), + (0b11001100, 0b00110011), (0b10000001, 0b01111110) + ] + + for a_val, b_val in test_pairs: + 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) + a_bits = a_bits.unsqueeze(0).expand(pop_size, -1) + b_bits = b_bits.unsqueeze(0).expand(pop_size, -1) + + ab_interleaved = torch.stack([a_bits, b_bits], dim=2).view(pop_size, 16) + + w_and = pop['alu.alu8bit.and.weight'].view(pop_size, -1) + b_and = pop['alu.alu8bit.and.bias'].view(pop_size, -1) + w_and_reshaped = w_and.view(pop_size, 8, 2) + and_result = heaviside((ab_interleaved.view(pop_size, 8, 2) * w_and_reshaped).sum(2) + b_and) + and_expected = torch.tensor([((a_val & b_val) >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (and_result == and_expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + w_or = pop['alu.alu8bit.or.weight'].view(pop_size, -1) + b_or = pop['alu.alu8bit.or.bias'].view(pop_size, -1) + w_or_reshaped = w_or.view(pop_size, 8, 2) + or_result = heaviside((ab_interleaved.view(pop_size, 8, 2) * w_or_reshaped).sum(2) + b_or) + or_expected = torch.tensor([((a_val | b_val) >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (or_result == or_expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + w_not = pop['alu.alu8bit.not.weight'].view(pop_size, -1) + b_not = pop['alu.alu8bit.not.bias'].view(pop_size, -1) + not_result = heaviside(a_bits * w_not + b_not) + not_expected = torch.tensor([((~a_val & 0xFF) >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (not_result == not_expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + w1_or = pop['alu.alu8bit.xor.layer1.or.weight'].view(pop_size, -1) + b1_or = pop['alu.alu8bit.xor.layer1.or.bias'].view(pop_size, -1) + w1_nand = pop['alu.alu8bit.xor.layer1.nand.weight'].view(pop_size, -1) + b1_nand = pop['alu.alu8bit.xor.layer1.nand.bias'].view(pop_size, -1) + w2 = pop['alu.alu8bit.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['alu.alu8bit.xor.layer2.bias'].view(pop_size, -1) + + h_or = heaviside((ab_interleaved.view(pop_size, 8, 2) * w1_or.view(pop_size, 8, 2)).sum(2) + b1_or) + h_nand = heaviside((ab_interleaved.view(pop_size, 8, 2) * w1_nand.view(pop_size, 8, 2)).sum(2) + b1_nand) + hidden = torch.stack([h_or, h_nand], dim=2) + xor_result = heaviside((hidden * w2.view(pop_size, 8, 2)).sum(2) + b2) + xor_expected = torch.tensor([((a_val ^ b_val) >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (xor_result == xor_expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + for val in [0x00, 0x01, 0x7F, 0x80, 0xFE, 0xFF, 0xAA, 0x55]: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + val_bits = val_bits.unsqueeze(0).expand(pop_size, -1) + + w_shl = pop['alu.alu8bit.shl.weight'].view(pop_size, -1) + shl_result = val_bits * w_shl + shl_expected = torch.tensor([(val >> (7-i)) & 1 if i > 0 else 0 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (shl_result == shl_expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + w_shr = pop['alu.alu8bit.shr.weight'].view(pop_size, -1) + shr_result = val_bits * w_shr + shr_expected = torch.tensor([(val >> (7-i)) & 1 if i < 7 else 0 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (shr_result == shr_expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + if debug and pop_size == 1: + print(f" ALU 8-bit Operations: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_alu_flags(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ALU FLAGS - Test Zero, Negative, Carry, Overflow flag computation. + Uses alu.aluflags.* tensors. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + zero_tests = [ + (0x00, True), (0x01, False), (0x80, False), (0xFF, False), + (0x7F, False), (0x10, False), (0x08, False), (0x04, False) + ] + + for val, expected_zero in zero_tests: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + val_bits = val_bits.unsqueeze(0).expand(pop_size, -1) + + w_zero = pop['alu.aluflags.zero.weight'].view(pop_size, -1) + b_zero = pop['alu.aluflags.zero.bias'].view(pop_size) + zero_flag = heaviside((val_bits * w_zero).sum(1) + b_zero) + expected = 1.0 if expected_zero else 0.0 + scores += (zero_flag == expected).float() + total_tests += 1 + + neg_tests = [ + (0x00, False), (0x01, False), (0x7F, False), (0x80, True), + (0x81, True), (0xFF, True), (0xFE, True), (0x40, False) + ] + + for val, expected_neg in neg_tests: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + val_bits = val_bits.unsqueeze(0).expand(pop_size, -1) + + w_neg = pop['alu.aluflags.negative.weight'].view(pop_size, -1) + b_neg = pop['alu.aluflags.negative.bias'].view(pop_size) + neg_flag = heaviside((val_bits * w_neg).sum(1) + b_neg) + expected = 1.0 if expected_neg else 0.0 + scores += (neg_flag == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" ALU Flags: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_alu_integration(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ALU INTEGRATION - End-to-end: opcode → operation → result → flags. + Tests the full ALU pipeline. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + alu_ops = [ + (0, 'AND', lambda a, b: a & b), + (1, 'OR', lambda a, b: a | b), + (2, 'XOR', lambda a, b: a ^ b), + (3, 'NOT', lambda a, b: (~a) & 0xFF), + (4, 'ADD', lambda a, b: (a + b) & 0xFF), + (5, 'SUB', lambda a, b: (a - b) & 0xFF), + (6, 'SHL', lambda a, b: (a << 1) & 0xFF), + (7, 'SHR', lambda a, b: (a >> 1) & 0xFF), + ] + + test_values = [ + (0x00, 0x00), (0xFF, 0xFF), (0xAA, 0x55), (0x0F, 0xF0), + (0x01, 0x01), (0x80, 0x7F), (0x12, 0x34), (0x00, 0xFF) + ] + + for opcode, op_name, op_fn in alu_ops: + opcode_bits = torch.tensor([(opcode >> (3-i)) & 1 for i in range(4)], + device=self.device, dtype=torch.float32) + opcode_bits = opcode_bits.unsqueeze(0).expand(pop_size, -1) + + op_selectors = [] + for op_idx in range(16): + w = pop[f'alu.alucontrol.op{op_idx}.weight'].view(pop_size, -1) + b = pop[f'alu.alucontrol.op{op_idx}.bias'].view(pop_size) + selector = heaviside((opcode_bits * w).sum(1) + b) + op_selectors.append(selector) + + expected_selector = opcode + scores += (op_selectors[expected_selector] == 1.0).float() + total_tests += 1 + + other_off = torch.ones(pop_size, device=self.device) + for i in range(16): + if i != expected_selector: + other_off *= (op_selectors[i] == 0.0).float() + scores += other_off + total_tests += 1 + + for a_val, b_val in test_values: + expected_result = op_fn(a_val, b_val) + + result_bits = torch.tensor([(expected_result >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + result_bits = result_bits.unsqueeze(0).expand(pop_size, -1) + + w_zero = pop['alu.aluflags.zero.weight'].view(pop_size, -1) + b_zero = pop['alu.aluflags.zero.bias'].view(pop_size) + zero_flag = heaviside((result_bits * w_zero).sum(1) + b_zero) + expected_zero = 1.0 if expected_result == 0 else 0.0 + scores += (zero_flag == expected_zero).float() + total_tests += 1 + + w_neg = pop['alu.aluflags.negative.weight'].view(pop_size, -1) + b_neg = pop['alu.aluflags.negative.bias'].view(pop_size) + neg_flag = heaviside((result_bits * w_neg).sum(1) + b_neg) + expected_neg = 1.0 if (expected_result & 0x80) else 0.0 + scores += (neg_flag == expected_neg).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" ALU Integration: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_cpu_instruction_cycle(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + CPU INSTRUCTION CYCLE - Test fetch/decode/execute with control flow. + Verifies conditional jumps interact correctly with ALU flags. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + flag_jump_tests = [ + ('jz', 0x00, True), + ('jz', 0x01, False), + ('jz', 0xFF, False), + ('jnz', 0x00, False), + ('jnz', 0x01, True), + ('jnz', 0xFF, True), + ('jn', 0x80, True), + ('jn', 0x7F, False), + ('jn', 0xFF, True), + ('jn', 0x00, False), + ('jp', 0x00, True), + ('jp', 0x01, True), + ('jp', 0x7F, True), + ('jp', 0x80, False), + ('jp', 0xFF, False), + ] + + for jump_type, alu_result, should_jump in flag_jump_tests: + result_bits = torch.tensor([(alu_result >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + result_bits = result_bits.unsqueeze(0).expand(pop_size, -1) + + if jump_type in ['jz', 'jnz']: + w_zero = pop['alu.aluflags.zero.weight'].view(pop_size, -1) + b_zero = pop['alu.aluflags.zero.bias'].view(pop_size) + flag = heaviside((result_bits * w_zero).sum(1) + b_zero) + condition = flag if jump_type == 'jz' else (1 - flag) + elif jump_type in ['jn', 'jp']: + w_neg = pop['alu.aluflags.negative.weight'].view(pop_size, -1) + b_neg = pop['alu.aluflags.negative.bias'].view(pop_size) + flag = heaviside((result_bits * w_neg).sum(1) + b_neg) + condition = flag if jump_type == 'jn' else (1 - flag) + + expected = 1.0 if should_jump else 0.0 + scores += (condition == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" CPU Instruction Cycle: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_cpu_programs(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + CPU PROGRAMS - Simulate simple programs using ALU and control flow. + Tests: countdown, accumulator, bit manipulation sequences. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + countdown_tests = [10, 5, 15, 1, 20] + for start_val in countdown_tests: + val = start_val + iterations = 0 + while val > 0 and iterations < 256: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + val_bits = val_bits.unsqueeze(0).expand(pop_size, -1) + + w_zero = pop['alu.aluflags.zero.weight'].view(pop_size, -1) + b_zero = pop['alu.aluflags.zero.bias'].view(pop_size) + is_zero = heaviside((val_bits * w_zero).sum(1) + b_zero) + + if is_zero[0].item() == 1.0: + break + + val = (val - 1) & 0xFF + iterations += 1 + + scores += float(iterations == start_val) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + accumulate_tests = [(1, 5), (3, 4), (10, 3), (7, 7)] + for add_val, count in accumulate_tests: + acc = 0 + for _ in range(count): + acc = (acc + add_val) & 0xFF + expected = (add_val * count) & 0xFF + + acc_bits = torch.tensor([(acc >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + exp_bits = torch.tensor([(expected >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (acc_bits == exp_bits).all().float() * torch.ones(pop_size, device=self.device) + total_tests += 1 + + shift_tests = [0x01, 0x80, 0xAA, 0x55, 0xFF, 0x7E] + for val in shift_tests: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + val_bits = val_bits.unsqueeze(0).expand(pop_size, -1) + + w_shl = pop['alu.alu8bit.shl.weight'].view(pop_size, -1) + after_shl_mask = (val_bits * w_shl) + + w_shr = pop['alu.alu8bit.shr.weight'].view(pop_size, -1) + after_both_masks = (after_shl_mask * w_shr) + + middle_bits = val & 0x7E + expected = torch.tensor([(middle_bits >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + scores += (after_both_masks == expected.unsqueeze(0)).float().sum(1) + total_tests += 8 + + if debug and pop_size == 1: + print(f" CPU Programs: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_alu_output_mux(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ALU OUTPUT MUX - Verify the output mux weights are non-zero. + This tensor selects which ALU operation's result to output. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + w_mux = pop['alu.alu8bit.output_mux.weight'].view(pop_size, -1) + scores += (w_mux.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" ALU Output Mux: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_alu_adder_and_carry(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ALU ADDER - Verify ALU's internal adder and carry/overflow flag weights are non-zero. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + w_add = pop['alu.alu8bit.add.weight'].view(pop_size, -1) + scores += (w_add.abs().sum(1) > 0).float() + total_tests += 1 + + w_carry = pop['alu.aluflags.carry.weight'].view(pop_size, -1) + scores += (w_carry.abs().sum(1) > 0).float() + total_tests += 1 + + w_overflow = pop['alu.aluflags.overflow.weight'].view(pop_size, -1) + scores += (w_overflow.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" ALU Adder & Carry/Overflow: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_control_stack_ops(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + CONTROL STACK - Test CALL/RET/PUSH/POP and stack pointer operations. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + stack_tensors = [ + 'control.call.jump', 'control.call.push', + 'control.ret.jump', 'control.ret.pop', + 'control.push.sp_dec', 'control.push.store', + 'control.pop.load', 'control.pop.sp_inc', + 'control.sp_dec.uses', 'control.sp_inc.uses', + ] + + for tensor_name in stack_tensors: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Control Stack Ops: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_jump_instructions(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + JUMP INSTRUCTIONS - Test that jump address bits are loaded correctly. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for bit in range(8): + w = pop[f'control.jump.bit{bit}.weight'].view(pop_size, -1) + b = pop[f'control.jump.bit{bit}.bias'].view(pop_size, -1) + + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + scores += (b.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Jump Instructions: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_error_detection(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + ERROR DETECTION - Test CRC, Hamming, parity, and checksum circuits. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + error_tensors = [ + 'error_detection.crc4.divisor', + 'error_detection.crc8.divisor', + 'error_detection.evenparitychecker.weight', + 'error_detection.oddparitychecker.parity.weight', + 'error_detection.oddparitychecker.not.weight', + 'error_detection.checksum8bit.sum.weight', + 'error_detection.hammingencode4bit.p0.weight', + 'error_detection.hammingencode4bit.p1.weight', + 'error_detection.hammingencode4bit.p2.weight', + 'error_detection.hammingencode4bit.p3.weight', + 'error_detection.hammingdecode7bit.s1.weight', + 'error_detection.hammingdecode7bit.s2.weight', + 'error_detection.hammingdecode7bit.s3.weight', + 'error_detection.hammingsyndrome.s1.weight', + 'error_detection.hammingsyndrome.s2.weight', + 'error_detection.hammingsyndrome.s3.weight', + 'error_detection.longitudinalparity.col_parity', + 'error_detection.longitudinalparity.row_parity', + ] + + for tensor_name in error_tensors: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + test_bytes = [0x00, 0xFF, 0xAA, 0x55, 0x0F, 0xF0, 0x12, 0x34] + for val in test_bytes: + val_bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + val_bits = val_bits.unsqueeze(0).expand(pop_size, -1) + + w_parity = pop['error_detection.evenparitychecker.weight'].view(pop_size, -1) + parity_sum = (val_bits * w_parity).sum(1) + expected_parity = bin(val).count('1') % 2 + scores += ((parity_sum % 2) == expected_parity).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Error Detection: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_combinational_logic(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + COMBINATIONAL LOGIC - Test barrel shifter, multiplexers, demultiplexers. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + w_barrel = pop['combinational.barrelshifter8bit.shift'].view(pop_size, -1) + scores += (w_barrel.abs().sum(1) > 0).float() + total_tests += 1 + + w_mux4 = pop['combinational.multiplexer4to1.select'].view(pop_size, -1) + scores += (w_mux4.abs().sum(1) > 0).float() + total_tests += 1 + + w_mux8 = pop['combinational.multiplexer8to1.select'].view(pop_size, -1) + scores += (w_mux8.abs().sum(1) > 0).float() + total_tests += 1 + + demux_tensors = [ + 'combinational.demultiplexer1to2.and0.weight', + 'combinational.demultiplexer1to2.and0.bias', + 'combinational.demultiplexer1to2.and1.weight', + 'combinational.demultiplexer1to2.and1.bias', + 'combinational.demultiplexer1to4.decode', + 'combinational.demultiplexer1to8.decode', + ] + + for tensor_name in demux_tensors: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Combinational Logic: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_pattern_recognition(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + PATTERN RECOGNITION - Test Hamming distance, one-hot, symmetry, alternating. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + pattern_tensors = [ + 'pattern_recognition.hammingdistance8bit.xor.weight', + 'pattern_recognition.hammingdistance8bit.popcount.weight', + 'pattern_recognition.onehotdetector.and.weight', + 'pattern_recognition.onehotdetector.and.bias', + 'pattern_recognition.onehotdetector.atleast1.weight', + 'pattern_recognition.onehotdetector.atleast1.bias', + 'pattern_recognition.onehotdetector.atmost1.weight', + 'pattern_recognition.onehotdetector.atmost1.bias', + 'pattern_recognition.symmetry8bit.xnor0.weight', + 'pattern_recognition.symmetry8bit.xnor1.weight', + 'pattern_recognition.symmetry8bit.xnor2.weight', + 'pattern_recognition.symmetry8bit.xnor3.weight', + 'pattern_recognition.symmetry8bit.and.weight', + 'pattern_recognition.symmetry8bit.and.bias', + 'pattern_recognition.alternating8bit.pattern1.weight', + 'pattern_recognition.alternating8bit.pattern2.weight', + 'pattern_recognition.runlength.weight', + ] + + for tensor_name in pattern_tensors: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Pattern Recognition: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_multiplier_stages(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + MULTIPLIER STAGES - Test internal stages of 8x8 multiplier. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for stage in range(7): + stage_tensors = [k for k in pop.keys() if f'multiplier8x8.stage{stage}' in k] + stage_mag = sum(pop[k].abs().sum().item() for k in stage_tensors) + scores += float(stage_mag > 0) * torch.ones(pop_size, device=self.device) + total_tests += 1 + + w_2x2 = [k for k in pop.keys() if 'multiplier2x2' in k] + for tensor_name in w_2x2[:4]: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Multiplier Stages: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_incrementer_decrementer(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + INCREMENTER/DECREMENTER - Test dedicated +1/-1 circuits. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + inc_tensors = [ + 'arithmetic.incrementer8bit.adder', + 'arithmetic.incrementer8bit.one', + ] + + for tensor_name in inc_tensors: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + dec_tensors = [ + 'arithmetic.decrementer8bit.adder', + 'arithmetic.decrementer8bit.neg_one', + ] + + for tensor_name in dec_tensors: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Incrementer/Decrementer: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_manifest(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + MANIFEST - Verify manifest values are preserved. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + manifest_tensors = [ + ('manifest.alu_operations', 16), + ('manifest.flags', 4), + ('manifest.instruction_width', 16), + ('manifest.memory_bytes', 256), + ('manifest.pc_width', 8), + ('manifest.register_width', 8), + ('manifest.registers', 4), + ('manifest.turing_complete', 1), + ('manifest.version', 1), + ] + + for tensor_name, expected_value in manifest_tensors: + w = pop[tensor_name].view(pop_size, -1) + actual_value = w.sum(1) + scores += (actual_value == expected_value).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Manifest: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_equality_circuit(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + EQUALITY CIRCUIT - Test 8-bit equality comparator using XNOR gates. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for i in range(8): + xnor_tensors = [ + f'arithmetic.equality8bit.xnor{i}.layer1.and.weight', + f'arithmetic.equality8bit.xnor{i}.layer1.and.bias', + f'arithmetic.equality8bit.xnor{i}.layer1.nor.weight', + f'arithmetic.equality8bit.xnor{i}.layer2.weight', + f'arithmetic.equality8bit.xnor{i}.layer2.bias', + ] + for tensor_name in xnor_tensors: + if tensor_name in pop: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Equality Circuit: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_minmax_circuits(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + MIN/MAX CIRCUITS - Test 8-bit min/max selectors. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + w_max = pop['arithmetic.max8bit.select'].view(pop_size, -1) + scores += (w_max.abs().sum(1) > 0).float() + total_tests += 1 + + w_min = pop['arithmetic.min8bit.select'].view(pop_size, -1) + scores += (w_min.abs().sum(1) > 0).float() + total_tests += 1 + + w_diff = pop['arithmetic.absolutedifference8bit.diff'].view(pop_size, -1) + scores += (w_diff.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Min/Max Circuits: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_ripplecarry_internal(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RIPPLE CARRY INTERNAL - Test internal components of ripple carry adders. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + rc_internal = [ + 'arithmetic.ripplecarry2bit.fa0.ha2.carry.weight', + 'arithmetic.ripplecarry2bit.fa0.ha2.carry.bias', + 'arithmetic.ripplecarry2bit.fa1.carry_or.weight', + 'arithmetic.ripplecarry2bit.fa1.carry_or.bias', + 'arithmetic.ripplecarry4bit.fa0.ha2.carry.weight', + 'arithmetic.ripplecarry4bit.fa3.carry_or.weight', + 'arithmetic.ripplecarry8bit.fa0.ha2.carry.weight', + 'arithmetic.ripplecarry8bit.fa7.carry_or.weight', + ] + + for tensor_name in rc_internal: + if tensor_name in pop: + w = pop[tensor_name].view(pop_size, -1) + scores += (w.abs().sum(1) > 0).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Ripple Carry Internal: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_all_alu_cpu(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """Run all ALU and CPU integration tests.""" + pop_size = next(iter(pop.values())).shape[0] + total_scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + if debug: + print("\n=== ALU & CPU INTEGRATION TESTS ===") + + opcode_scores, opcode_tests = self._test_alu_opcode_decoder(pop, debug) + total_scores += opcode_scores + total_tests += opcode_tests + + ops_scores, ops_tests = self._test_alu_8bit_operations(pop, debug) + total_scores += ops_scores + total_tests += ops_tests + + flags_scores, flags_tests = self._test_alu_flags(pop, debug) + total_scores += flags_scores + total_tests += flags_tests + + integ_scores, integ_tests = self._test_alu_integration(pop, debug) + total_scores += integ_scores + total_tests += integ_tests + + cycle_scores, cycle_tests = self._test_cpu_instruction_cycle(pop, debug) + total_scores += cycle_scores + total_tests += cycle_tests + + prog_scores, prog_tests = self._test_cpu_programs(pop, debug) + total_scores += prog_scores + total_tests += prog_tests + + mux_scores, mux_tests = self._test_alu_output_mux(pop, debug) + total_scores += mux_scores + total_tests += mux_tests + + adder_scores, adder_tests = self._test_alu_adder_and_carry(pop, debug) + total_scores += adder_scores + total_tests += adder_tests + + stack_scores, stack_tests = self._test_control_stack_ops(pop, debug) + total_scores += stack_scores + total_tests += stack_tests + + jump_scores, jump_tests = self._test_jump_instructions(pop, debug) + total_scores += jump_scores + total_tests += jump_tests + + error_scores, error_tests = self._test_error_detection(pop, debug) + total_scores += error_scores + total_tests += error_tests + + comb_scores, comb_tests = self._test_combinational_logic(pop, debug) + total_scores += comb_scores + total_tests += comb_tests + + pattern_scores, pattern_tests = self._test_pattern_recognition(pop, debug) + total_scores += pattern_scores + total_tests += pattern_tests + + mult_scores, mult_tests = self._test_multiplier_stages(pop, debug) + total_scores += mult_scores + total_tests += mult_tests + + incdec_scores, incdec_tests = self._test_incrementer_decrementer(pop, debug) + total_scores += incdec_scores + total_tests += incdec_tests + + manifest_scores, manifest_tests = self._test_manifest(pop, debug) + total_scores += manifest_scores + total_tests += manifest_tests + + eq_scores, eq_tests = self._test_equality_circuit(pop, debug) + total_scores += eq_scores + total_tests += eq_tests + + minmax_scores, minmax_tests = self._test_minmax_circuits(pop, debug) + total_scores += minmax_scores + total_tests += minmax_tests + + rc_scores, rc_tests = self._test_ripplecarry_internal(pop, debug) + total_scores += rc_scores + total_tests += rc_tests + + if debug and pop_size == 1: + print(f" TOTAL ALU/CPU: {int(total_scores[0].item())}/{total_tests}") + + return total_scores, total_tests + + # ========================================================================= + # RANDOMIZED TESTS - Fresh cases each evaluation to prevent memorization + # ========================================================================= + + def _test_random_fibonacci(self, pop: Dict, n_sequences: int = 5, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED FIBONACCI - Random starting pairs, verify recurrence. + Tests F(n) = F(n-1) + F(n-2) structure with random seeds. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for _ in range(n_sequences): + f0 = torch.randint(0, 50, (1,), device=self.device).item() + f1 = torch.randint(1, 50, (1,), device=self.device).item() + + fib = [torch.full((pop_size,), float(f0), device=self.device), + torch.full((pop_size,), float(f1), device=self.device)] + expected = [f0, f1] + + for i in range(2, 10): + exp_val = (expected[i-1] + expected[i-2]) & 0xFF + expected.append(exp_val) + + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + for bit in range(8): + a_bit = ((fib[i-1].long() >> bit) & 1).float() + b_bit = ((fib[i-2].long() >> bit) & 1).float() + sum_bit, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry8bit.fa{bit}', + a_bit, b_bit, carry) + sum_bits.append(sum_bit) + result = sum(sum_bits[j] * (2**j) for j in range(8)) + fib.append(result) + + for i, exp in enumerate(expected): + scores += (fib[i] == exp).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Random Fibonacci: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_random_addition(self, pop: Dict, n_tests: int = 50, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED ADDITION - Random 8-bit operand pairs. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for _ in range(n_tests): + a = torch.randint(0, 256, (1,), device=self.device).item() + b = torch.randint(0, 256, (1,), device=self.device).item() + expected = (a + b) & 0xFF + + carry = torch.zeros(pop_size, device=self.device) + sum_bits = [] + for bit in range(8): + a_bit = ((a >> bit) & 1) * torch.ones(pop_size, device=self.device) + b_bit = ((b >> bit) & 1) * torch.ones(pop_size, device=self.device) + sum_bit, carry = self._eval_single_fa(pop, f'arithmetic.ripplecarry8bit.fa{bit}', + a_bit, b_bit, carry) + sum_bits.append(sum_bit) + result = sum(sum_bits[j] * (2**j) for j in range(8)) + scores += (result == expected).float() + + if debug and pop_size == 1: + print(f" Random Addition: {int(scores[0].item())}/{n_tests}") + + return scores, n_tests + + def _test_random_comparison(self, pop: Dict, n_tests: int = 50, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED COMPARISON - Random pairs tested against all comparators. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + for _ in range(n_tests): + a = torch.randint(0, 256, (1,), device=self.device).item() + b = torch.randint(0, 256, (1,), device=self.device).item() + + 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) + + for comp, op in [('greaterthan8bit', lambda x,y: x>y), + ('lessthan8bit', lambda x,y: x=y), + ('lessorequal8bit', lambda x,y: x<=y)]: + diff = (a_bits - b_bits).unsqueeze(0).expand(pop_size, -1) + if 'less' in comp: + diff = (b_bits - a_bits).unsqueeze(0).expand(pop_size, -1) + w = pop[f'arithmetic.{comp}.comparator'].view(pop_size, -1) + score = (diff * w).sum(1) + if 'equal' in comp: + result = (score >= 0).float() + else: + result = (score > 0).float() + expected = float(op(a, b)) + scores += (result == expected).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Random Comparison: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_random_hamming(self, pop: Dict, n_tests: int = 20, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED HAMMING DISTANCE - Random pairs, verify XOR + popcount. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + for _ in range(n_tests): + a = torch.randint(0, 256, (1,), device=self.device).item() + b = torch.randint(0, 256, (1,), device=self.device).item() + expected = bin(a ^ b).count('1') + + 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) + + xor_bits = [] + for i in range(8): + a_i = a_bits[i].unsqueeze(0).expand(pop_size) + b_i = b_bits[i].unsqueeze(0).expand(pop_size) + inp = torch.stack([a_i, b_i], dim=1) + + w1_n1 = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_n1 = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_n2 = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_n2 = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h1 = heaviside((inp * w1_n1).sum(1) + b1_n1) + h2 = heaviside((inp * w1_n2).sum(1) + b1_n2) + hidden = torch.stack([h1, h2], dim=1) + xor_bits.append(heaviside((hidden * w2).sum(1) + b2)) + + xor_stack = torch.stack(xor_bits, dim=1) + w_pop = pop['pattern_recognition.popcount.weight'].view(pop_size, -1) + b_pop = pop['pattern_recognition.popcount.bias'].view(pop_size) + hamming = (xor_stack * w_pop).sum(1) + b_pop + + scores += (hamming == expected).float() + + if debug and pop_size == 1: + print(f" Random Hamming: {int(scores[0].item())}/{n_tests}") + + return scores, n_tests + + def _test_random_sorting(self, pop: Dict, n_tests: int = 15, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED SORTING - Random 4-element arrays through bitonic network. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + def compare_swap_asc(a_val, b_val): + a_bits = torch.stack([((a_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + b_bits = torch.stack([((b_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + diff = a_bits - b_bits + w = pop['arithmetic.greaterthan8bit.comparator'].view(pop_size, -1) + should_swap = ((diff * w).sum(1) > 0).float() + return (a_val * (1 - should_swap) + b_val * should_swap, + b_val * (1 - should_swap) + a_val * should_swap) + + def compare_swap_desc(a_val, b_val): + a_bits = torch.stack([((a_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + b_bits = torch.stack([((b_val.long() >> (7-i)) & 1).float() for i in range(8)], dim=1) + diff = b_bits - a_bits + w = pop['arithmetic.lessthan8bit.comparator'].view(pop_size, -1) + should_swap = ((diff * w).sum(1) > 0).float() + return (a_val * (1 - should_swap) + b_val * should_swap, + b_val * (1 - should_swap) + a_val * should_swap) + + for _ in range(n_tests): + arr = [torch.randint(0, 256, (1,), device=self.device).item() for _ in range(4)] + expected = sorted(arr) + + a = torch.full((pop_size,), float(arr[0]), device=self.device) + b = torch.full((pop_size,), float(arr[1]), device=self.device) + c = torch.full((pop_size,), float(arr[2]), device=self.device) + d = torch.full((pop_size,), float(arr[3]), device=self.device) + + a, b = compare_swap_asc(a, b) + c, d = compare_swap_desc(c, d) + a, c = compare_swap_asc(a, c) + b, d = compare_swap_asc(b, d) + a, b = compare_swap_asc(a, b) + c, d = compare_swap_asc(c, d) + + match = ((a == expected[0]) & (b == expected[1]) & + (c == expected[2]) & (d == expected[3])).float() + scores += match + + if debug and pop_size == 1: + print(f" Random Sorting: {int(scores[0].item())}/{n_tests}") + + return scores, n_tests + + def _test_random_modular(self, pop: Dict, n_tests: int = 30, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED MODULAR - Random values against random moduli. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + moduli = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + for _ in range(n_tests): + val = torch.randint(0, 256, (1,), device=self.device).item() + mod = moduli[torch.randint(0, len(moduli), (1,)).item()] + expected = (val % mod == 0) + + bits = torch.tensor([(val >> (7-i)) & 1 for i in range(8)], + device=self.device, dtype=torch.float32) + bits_exp = bits.unsqueeze(0).expand(pop_size, -1) + + if mod in [2, 4, 8]: + w = pop[f'modular.mod{mod}.weight'].view(pop_size, -1) + b = pop[f'modular.mod{mod}.bias'].view(pop_size) + result = heaviside((bits_exp * w).sum(1) + b) + else: + weights = [(2**(7-i)) % mod for i in range(8)] + max_sum = sum(weights) + divisible_sums = [k for k in range(0, max_sum + 1) if k % mod == 0] + num_detectors = len(divisible_sums) + + layer1_outputs = [] + for idx in range(num_detectors): + w_geq = pop[f'modular.mod{mod}.layer1.geq{idx}.weight'].view(pop_size, -1) + b_geq = pop[f'modular.mod{mod}.layer1.geq{idx}.bias'].view(pop_size) + w_leq = pop[f'modular.mod{mod}.layer1.leq{idx}.weight'].view(pop_size, -1) + b_leq = pop[f'modular.mod{mod}.layer1.leq{idx}.bias'].view(pop_size) + geq = heaviside((bits_exp * w_geq).sum(1) + b_geq) + leq = heaviside((bits_exp * w_leq).sum(1) + b_leq) + layer1_outputs.append((geq, leq)) + + layer2_outputs = [] + for idx in range(num_detectors): + w_eq = pop[f'modular.mod{mod}.layer2.eq{idx}.weight'].view(pop_size, -1) + b_eq = pop[f'modular.mod{mod}.layer2.eq{idx}.bias'].view(pop_size) + geq, leq = layer1_outputs[idx] + combined = torch.stack([geq, leq], dim=1) + layer2_outputs.append(heaviside((combined * w_eq).sum(1) + b_eq)) + + layer2_stack = torch.stack(layer2_outputs, dim=1) + w_or = pop[f'modular.mod{mod}.layer3.or.weight'].view(pop_size, -1) + b_or = pop[f'modular.mod{mod}.layer3.or.bias'].view(pop_size) + result = heaviside((layer2_stack * w_or).sum(1) + b_or) + + scores += (result == float(expected)).float() + total_tests += 1 + + if debug and pop_size == 1: + print(f" Random Modular: {int(scores[0].item())}/{total_tests}") + + return scores, total_tests + + def _test_random_cipher(self, pop: Dict, n_tests: int = 10, debug: bool = False) -> Tuple[torch.Tensor, int]: + """ + RANDOMIZED XOR CIPHER - Random plaintexts and keys. + """ + pop_size = next(iter(pop.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + + def xor_byte(byte_val, key_val): + result_bits = [] + for i in range(8): + b_bit = torch.full((pop_size,), float((byte_val >> (7-i)) & 1), device=self.device) + k_bit = torch.full((pop_size,), float((key_val >> (7-i)) & 1), device=self.device) + inp = torch.stack([b_bit, k_bit], dim=1) + + w1_n1 = pop['boolean.xor.layer1.neuron1.weight'].view(pop_size, -1) + b1_n1 = pop['boolean.xor.layer1.neuron1.bias'].view(pop_size) + w1_n2 = pop['boolean.xor.layer1.neuron2.weight'].view(pop_size, -1) + b1_n2 = pop['boolean.xor.layer1.neuron2.bias'].view(pop_size) + w2 = pop['boolean.xor.layer2.weight'].view(pop_size, -1) + b2 = pop['boolean.xor.layer2.bias'].view(pop_size) + + h1 = heaviside((inp * w1_n1).sum(1) + b1_n1) + h2 = heaviside((inp * w1_n2).sum(1) + b1_n2) + hidden = torch.stack([h1, h2], dim=1) + result_bits.append(heaviside((hidden * w2).sum(1) + b2)) + + return sum(result_bits[i] * (2**(7-i)) for i in range(8)) + + for _ in range(n_tests): + plaintext_len = torch.randint(2, 8, (1,)).item() + plaintext = [torch.randint(0, 256, (1,)).item() for _ in range(plaintext_len)] + key = torch.randint(0, 256, (1,)).item() + + all_match = torch.ones(pop_size, device=self.device) + for byte in plaintext: + cipher = byte ^ key + decrypted = xor_byte(cipher, key) + all_match *= (decrypted == byte).float() + scores += all_match + + if debug and pop_size == 1: + print(f" Random Cipher: {int(scores[0].item())}/{n_tests}") + + return scores, n_tests + + def _test_all_randomized(self, pop: Dict, debug: bool = False) -> Tuple[torch.Tensor, int]: + """Run all randomized tests - fresh cases each evaluation.""" + pop_size = next(iter(pop.values())).shape[0] + total_scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + + if debug: + print("\n=== RANDOMIZED TESTS (Fresh Cases Each Eval) ===") + + fib_scores, fib_tests = self._test_random_fibonacci(pop, n_sequences=5, debug=debug) + total_scores += fib_scores + total_tests += fib_tests + + add_scores, add_tests = self._test_random_addition(pop, n_tests=50, debug=debug) + total_scores += add_scores + total_tests += add_tests + + cmp_scores, cmp_tests = self._test_random_comparison(pop, n_tests=50, debug=debug) + total_scores += cmp_scores + total_tests += cmp_tests + + ham_scores, ham_tests = self._test_random_hamming(pop, n_tests=20, debug=debug) + total_scores += ham_scores + total_tests += ham_tests + + sort_scores, sort_tests = self._test_random_sorting(pop, n_tests=15, debug=debug) + total_scores += sort_scores + total_tests += sort_tests + + mod_scores, mod_tests = self._test_random_modular(pop, n_tests=30, debug=debug) + total_scores += mod_scores + total_tests += mod_tests + + cipher_scores, cipher_tests = self._test_random_cipher(pop, n_tests=10, debug=debug) + total_scores += cipher_scores + total_tests += cipher_tests + + if debug and pop_size == 1: + print(f" TOTAL RANDOMIZED: {int(total_scores[0].item())}/{total_tests}") + + return total_scores, total_tests + + # ========================================================================= + # MAIN EVALUATE + # ========================================================================= + + def evaluate(self, population: Dict[str, torch.Tensor], debug: bool = False) -> torch.Tensor: + """Evaluate fitness for entire population.""" + pop_size = next(iter(population.values())).shape[0] + scores = torch.zeros(pop_size, device=self.device) + total_tests = 0 + self.category_scores = {} # Track per-category scores for debugging + + # ================================================================= + # BOOLEAN GATES (34 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + for gate in ['and', 'or', 'nand', 'nor']: + scores += self._test_single_gate(population, gate, self.tt2, self.expected[gate]) + total_tests += 4 + cat_tests += 4 + + # NOT + w = population['boolean.not.weight'].view(pop_size, -1) + b = population['boolean.not.bias'].view(pop_size) + out = heaviside(self.not_inputs @ w.T + b) + scores += (out == self.expected['not'].unsqueeze(1)).float().sum(0) + total_tests += 2 + cat_tests += 2 + + # IMPLIES + scores += self._test_single_gate(population, 'implies', self.tt2, self.expected['implies']) + total_tests += 4 + cat_tests += 4 + + # XOR, XNOR, BIIMPLIES + scores += self._test_twolayer_gate(population, 'boolean.xor', self.tt2, self.expected['xor']) + scores += self._test_twolayer_gate(population, 'boolean.xnor', self.tt2, self.expected['xnor']) + scores += self._test_twolayer_gate(population, 'boolean.biimplies', self.tt2, self.expected['biimplies']) + total_tests += 12 + cat_tests += 12 + self.category_scores['boolean'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # ARITHMETIC - ADDERS (370 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + + sub_start = scores.clone() + scores += self._test_halfadder(population) + total_tests += 8 + cat_tests += 8 + self.category_scores['halfadder'] = ((scores - sub_start)[0].item(), 8) + + sub_start = scores.clone() + scores += self._test_fulladder(population) + total_tests += 16 + cat_tests += 16 + self.category_scores['fulladder'] = ((scores - sub_start)[0].item(), 16) + + # Ripple carry adders + sub_start = scores.clone() + rc2_tests = [(a, b) for a in range(4) for b in range(4)] + scores += self._test_ripplecarry(population, 2, rc2_tests) + total_tests += 16 + cat_tests += 16 + self.category_scores['ripplecarry2'] = ((scores - sub_start)[0].item(), 16) + + sub_start = scores.clone() + rc4_tests = [(a, b) for a in range(16) for b in range(16)] + scores += self._test_ripplecarry(population, 4, rc4_tests) + total_tests += 256 + cat_tests += 256 + self.category_scores['ripplecarry4'] = ((scores - sub_start)[0].item(), 256) + + sub_start = scores.clone() + rc8_tests = [(0,0), (1,1), (127,128), (255,1), (128,127), (255,255), + (0xAA, 0x55), (0x0F, 0xF0), (100, 155), (200, 55)] + scores += self._test_ripplecarry(population, 8, rc8_tests) + total_tests += len(rc8_tests) + cat_tests += len(rc8_tests) + self.category_scores['ripplecarry8'] = ((scores - sub_start)[0].item(), len(rc8_tests)) + + # 8x8 Multiplier (62 unique test pairs) + sub_start = scores.clone() + scores += self._test_multiplier8x8(population, debug=debug) + total_tests += 62 + cat_tests += 62 + self.category_scores['multiplier8x8'] = ((scores - sub_start)[0].item(), 62) + + self.category_scores['arithmetic_adders'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # ARITHMETIC - COMPARATORS (300 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + scores += self._test_comparator(population, 'greaterthan8bit', 'gt') + scores += self._test_comparator(population, 'lessthan8bit', 'lt') + scores += self._test_comparator(population, 'greaterorequal8bit', 'geq') + scores += self._test_comparator(population, 'lessorequal8bit', 'leq') + total_tests += 4 * len(self.comp_a) + cat_tests += 4 * len(self.comp_a) + + scores += self._test_equality(population) + total_tests += len(self.comp_a) + cat_tests += len(self.comp_a) + self.category_scores['comparators'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # THRESHOLD GATES (312 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + for k, name in enumerate(['oneoutof8', 'twooutof8', 'threeoutof8', 'fouroutof8', + 'fiveoutof8', 'sixoutof8', 'sevenoutof8', 'alloutof8'], 1): + scores += self._test_threshold_kofn(population, k, name) + total_tests += len(self.test_8bit) + cat_tests += len(self.test_8bit) + + scores += self._test_majority(population) + scores += self._test_minority(population) + total_tests += 2 * len(self.test_8bit) + cat_tests += 2 * len(self.test_8bit) + + scores += self._test_atleastk(population, 4) + scores += self._test_atmostk(population, 4) + scores += self._test_exactlyk(population, 4) + total_tests += 3 * len(self.test_8bit) + cat_tests += 3 * len(self.test_8bit) + self.category_scores['threshold'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # PATTERN RECOGNITION (72 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + scores += self._test_popcount(population) + scores += self._test_allzeros(population) + scores += self._test_allones(population) + total_tests += 3 * len(self.test_8bit) + cat_tests += 3 * len(self.test_8bit) + self.category_scores['pattern'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # ERROR DETECTION (48 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + scores += self._test_parity(population, 'paritychecker8bit', True) + scores += self._test_parity(population, 'paritygenerator8bit', True) + total_tests += 2 * len(self.test_8bit) + cat_tests += 2 * len(self.test_8bit) + self.category_scores['error_detection'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # MODULAR ARITHMETIC (2816 tests: 256 values × 11 moduli) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + for mod in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]: + scores += self._test_modular(population, mod) + total_tests += len(self.mod_test) + cat_tests += len(self.mod_test) + self.category_scores['modular'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # COMBINATIONAL (96 tests) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + scores += self._test_mux2to1(population) + total_tests += 8 + cat_tests += 8 + + scores += self._test_decoder3to8(population) + total_tests += 64 + cat_tests += 64 + + scores += self._test_encoder8to3(population) + total_tests += 24 + cat_tests += 24 + self.category_scores['combinational'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # CONTROL FLOW (432 tests: 9 circuits × 6 cases × 8 bits) + # ================================================================= + cat_start = scores.clone() + cat_tests = 0 + for ctrl in ['conditionaljump', 'jz', 'jnz', 'jc', 'jnc', 'jn', 'jp', 'jv', 'jnv']: + scores += self._test_conditional_jump(population, ctrl) + total_tests += 6 * 8 + cat_tests += 6 * 8 + self.category_scores['control_flow'] = ((scores - cat_start)[0].item(), cat_tests) + + # ================================================================= + # GAME TESTS - Composite Circuit Verification + # ================================================================= + game_scores, game_tests = self._test_all_games(population, debug) + scores += game_scores + total_tests += game_tests + self.category_scores['games'] = (game_scores[0].item() if pop_size == 1 else 0, game_tests) + + # ================================================================= + # BESPOKE TESTS - Novel Circuit Compositions + # ================================================================= + bespoke_scores, bespoke_tests = self._test_all_bespoke(population, debug) + scores += bespoke_scores + total_tests += bespoke_tests + self.category_scores['bespoke'] = (bespoke_scores[0].item() if pop_size == 1 else 0, bespoke_tests) + + # ================================================================= + # ALU & CPU INTEGRATION TESTS - Verify integrated CPU functionality + # ================================================================= + alu_cpu_scores, alu_cpu_tests = self._test_all_alu_cpu(population, debug) + scores += alu_cpu_scores + total_tests += alu_cpu_tests + self.category_scores['alu_cpu'] = (alu_cpu_scores[0].item() if pop_size == 1 else 0, alu_cpu_tests) + + # ================================================================= + # RANDOMIZED TESTS - Fresh cases each evaluation + # ================================================================= + random_scores, random_tests = self._test_all_randomized(population, debug) + scores += random_scores + total_tests += random_tests + self.category_scores['randomized'] = (random_scores[0].item() if pop_size == 1 else 0, random_tests) + + self.total_tests = total_tests + + if debug and pop_size == 1: + print("\n=== DEBUG: Per-category results ===") + for cat, (got, expected) in self.category_scores.items(): + status = "PASS" if got == expected else "FAIL" + print(f" {cat}: {int(got)}/{expected} [{status}]") + + return scores / total_tests + + +def create_population(base_tensors: Dict[str, torch.Tensor], + pop_size: int, + device='cuda') -> Dict[str, torch.Tensor]: + """Create population by replicating base tensors.""" + population = {} + for name, weight in base_tensors.items(): + population[name] = weight.unsqueeze(0).expand(pop_size, *weight.shape).clone().to(device) + return population + + +if __name__ == "__main__": + import time + import argparse + + parser = argparse.ArgumentParser(description='Iron Eval - Threshold Computer Test Suite') + parser.add_argument('--training', action='store_true', + help='Training mode: enable batched population evaluation') + parser.add_argument('--pop_size', type=int, default=1, + help='Population size for training mode (default: 1)') + parser.add_argument('--device', type=str, default='cuda', + help='Device to use: cuda or cpu (default: cuda)') + parser.add_argument('--quiet', action='store_true', + help='Suppress detailed output') + args = parser.parse_args() + + if args.training and args.pop_size == 1: + args.pop_size = 10000 + + print("="*70) + if args.training: + print(f" IRON EVAL - TRAINING MODE (pop_size={args.pop_size})") + else: + print(" IRON EVAL - EVALUATION MODE") + print("="*70) + + print("\nLoading model...") + model = load_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) + + print(f"\nCreating population (size {args.pop_size})...") + pop = 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(pop, debug=not args.quiet) + if args.device == 'cuda': + torch.cuda.synchronize() + elapsed = time.perf_counter() - start + + print(f"\nResults:") + if args.training: + print(f" Mean Fitness: {fitness.mean().item():.6f}") + print(f" Min Fitness: {fitness.min().item():.6f}") + print(f" Max Fitness: {fitness.max().item():.6f}") + else: + print(f" Fitness: {fitness[0]:.6f}") + print(f" Total tests: {evaluator.total_tests}") + print(f" Time: {elapsed*1000:.2f} ms") + if args.training: + print(f" Throughput: {args.pop_size / elapsed:.0f} evals/sec") + + if args.training: + perfect = (fitness == 1.0).sum().item() + print(f"\n Perfect individuals: {perfect}/{args.pop_size}") + else: + if fitness[0] == 1.0: + print("\n STATUS: PASS - All circuits functional") + else: + failed = int((1 - fitness[0]) * evaluator.total_tests) + print(f"\n STATUS: FAIL - {failed} tests failed")