| |
| """ |
| Unified evaluator for threshold-calculus circuits. |
| |
| Usage: |
| python eval.py # Run all tests (always full + verbose) |
| python eval.py --circuit float16.add # Run specific circuit |
| python eval.py --json # Output JSON for CI |
| python eval.py --coverage # Show detailed coverage report |
| python eval.py --inputs-coverage # Sweep all gates using .inputs tensors |
| python eval.py --list # List available categories/circuits |
| """ |
|
|
| import argparse |
| import json |
| import math |
| import random |
| import struct |
| import sys |
| import time |
| from dataclasses import dataclass, field |
| from typing import Dict, List, Optional, Tuple, Callable, Any |
|
|
| import torch |
| from safetensors import safe_open |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class TestResult: |
| circuit: str |
| passed: int |
| total: int |
| failures: List[Dict[str, Any]] = field(default_factory=list) |
|
|
| @property |
| def success(self) -> bool: |
| return self.passed == self.total |
|
|
| @property |
| def pct(self) -> float: |
| return 100.0 * self.passed / self.total if self.total > 0 else 0.0 |
|
|
|
|
| @dataclass |
| class EvalContext: |
| tensors: Dict[str, torch.Tensor] |
| gates: List[str] |
| signals: Dict[str, int] |
| name_to_id: Dict[str, int] = field(default_factory=dict) |
| id_to_name: Dict[int, str] = field(default_factory=dict) |
| verbose: bool = False |
| quick: bool = False |
| tested_tensors: set = field(default_factory=set) |
| alias_to_gate: Dict[int, int] = field(default_factory=dict) |
| gate_to_alias: Dict[int, List[int]] = field(default_factory=dict) |
| alias_ready: bool = False |
| topo_cache: Dict[str, List[str]] = field(default_factory=dict) |
|
|
|
|
| def load_model(path: str = "./arithmetic.safetensors") -> Tuple[Dict[str, torch.Tensor], List[str], Dict[str, int], Dict[str, int], Dict[int, str]]: |
| """Load model and extract gates and signals.""" |
| tensors = {} |
| name_to_id = {} |
| id_to_name = {} |
| with safe_open(path, framework='pt') as f: |
| for name in f.keys(): |
| tensors[name] = f.get_tensor(name) |
| metadata = f.metadata() |
| if metadata and 'signal_registry' in metadata: |
| registry_raw = json.loads(metadata['signal_registry']) |
| name_to_id = {v: int(k) for k, v in registry_raw.items()} |
| id_to_name = {int(k): v for k, v in registry_raw.items()} |
|
|
| |
| gates = sorted(set(k.rsplit('.', 1)[0] for k in tensors.keys() if k.endswith('.weight'))) |
|
|
| |
| signals = {} |
| signal_id = 0 |
| for gate in gates: |
| signals[gate] = signal_id |
| signal_id += 1 |
|
|
| return tensors, gates, signals, name_to_id, id_to_name |
|
|
|
|
| def evaluate_gate(ctx: EvalContext, gate: str, inputs: torch.Tensor) -> torch.Tensor: |
| """Evaluate a single threshold gate.""" |
| weight_key = f"{gate}.weight" |
| bias_key = f"{gate}.bias" |
|
|
| if weight_key not in ctx.tensors: |
| raise ValueError(f"Gate not found: {gate}") |
|
|
| ctx.tested_tensors.add(weight_key) |
| if bias_key in ctx.tensors: |
| ctx.tested_tensors.add(bias_key) |
|
|
| weight = ctx.tensors[weight_key] |
| bias = ctx.tensors.get(bias_key, torch.tensor([0.0])) |
|
|
| |
| result = torch.matmul(inputs.float(), weight.float()) + bias.float() |
| return (result >= 0).float() |
|
|
|
|
| def evaluate_circuit(ctx: EvalContext, prefix: str, input_bits: torch.Tensor, |
| output_gates: List[str]) -> torch.Tensor: |
| """Evaluate a circuit with explicit gate ordering from routing.""" |
| |
| circuit_gates = [g for g in ctx.gates if g.startswith(prefix + ".")] |
|
|
| |
| signals = {} |
|
|
| |
| for i in range(input_bits.shape[-1]): |
| signals[f"${chr(ord('a') + i // 16)}[{i % 16}]"] = input_bits[..., i] |
|
|
| |
| if input_bits.shape[-1] <= 32: |
| half = input_bits.shape[-1] // 2 |
| for i in range(half): |
| signals[f"$a[{i}]"] = input_bits[..., i] |
| for i in range(half, input_bits.shape[-1]): |
| signals[f"$b[{i - half}]"] = input_bits[..., i] |
|
|
| |
| for gate in circuit_gates: |
| inputs_key = f"{gate}.inputs" |
| if inputs_key in ctx.tensors: |
| ctx.tested_tensors.add(inputs_key) |
| input_ids = ctx.tensors[inputs_key].tolist() |
| |
| gate_inputs = [] |
| for sig_id in input_ids: |
| |
| for sig_name, sig_val in signals.items(): |
| if hash(sig_name) % 10000 == sig_id % 10000: |
| gate_inputs.append(sig_val) |
| break |
|
|
| |
| weight = ctx.tensors.get(f"{gate}.weight") |
| bias = ctx.tensors.get(f"{gate}.bias", torch.tensor([0.0])) |
| if weight is not None: |
| ctx.tested_tensors.add(f"{gate}.weight") |
| ctx.tested_tensors.add(f"{gate}.bias") |
|
|
| |
| outputs = [] |
| for out_gate in output_gates: |
| if out_gate in signals: |
| outputs.append(signals[out_gate]) |
| else: |
| outputs.append(torch.zeros_like(input_bits[..., 0])) |
|
|
| return torch.stack(outputs, dim=-1) if outputs else torch.tensor([]) |
|
|
|
|
| def seed_external_signals(ctx: EvalContext, rng: random.Random, |
| extra_names: Optional[List[str]] = None) -> Dict[int, float]: |
| """Seed external input signals and constants with random 0/1 values.""" |
| signals: Dict[int, float] = {} |
|
|
| |
| if "#0" in ctx.name_to_id: |
| signals[ctx.name_to_id["#0"]] = 0.0 |
| if "#1" in ctx.name_to_id: |
| signals[ctx.name_to_id["#1"]] = 1.0 |
|
|
| |
| for name, sid in ctx.name_to_id.items(): |
| if name.startswith("$") or ".$" in name: |
| if sid not in signals: |
| signals[sid] = float(rng.getrandbits(1)) |
|
|
| if extra_names: |
| for name in extra_names: |
| sid = ctx.name_to_id.get(name) |
| if sid is not None and sid not in signals: |
| signals[sid] = float(rng.getrandbits(1)) |
|
|
| return signals |
|
|
|
|
| def resolve_alias_target(name: str, gates: set) -> Optional[str]: |
| """Resolve common alias signal names to actual gate names.""" |
| if name in gates: |
| return name |
| cand = name + ".layer2" |
| if cand in gates: |
| return cand |
| if name.endswith(".sum"): |
| cand = name[:-4] + ".xor2.layer2" |
| if cand in gates: |
| return cand |
| if name.endswith(".cout"): |
| for suffix in [".or_carry", ".carry_or"]: |
| cand = name[:-5] + suffix |
| if cand in gates: |
| return cand |
| return None |
|
|
|
|
| def build_alias_maps(ctx: EvalContext) -> Tuple[Dict[int, int], Dict[int, List[int]]]: |
| """Build alias maps from orphan signals to actual gate outputs.""" |
| gates = set(ctx.gates) |
| alias_to_gate: Dict[int, int] = {} |
| gate_to_alias: Dict[int, List[int]] = {} |
|
|
| for name, sid in ctx.name_to_id.items(): |
| if name in ("#0", "#1"): |
| continue |
| if name.startswith("$") or ".$" in name: |
| continue |
| if name in gates: |
| continue |
| target = resolve_alias_target(name, gates) |
| if not target: |
| continue |
| target_id = ctx.name_to_id.get(target) |
| if target_id is None: |
| continue |
| alias_to_gate[sid] = target_id |
| gate_to_alias.setdefault(target_id, []).append(sid) |
|
|
| return alias_to_gate, gate_to_alias |
|
|
|
|
| def topo_sort_gates(ctx: EvalContext, gate_list: List[str]) -> List[str]: |
| """Topologically sort gates based on .inputs dependencies.""" |
| gate_set = set(gate_list) |
| deps: Dict[str, set] = {g: set() for g in gate_list} |
| rev: Dict[str, List[str]] = {g: [] for g in gate_list} |
|
|
| for gate in gate_list: |
| inputs_key = f"{gate}.inputs" |
| if inputs_key not in ctx.tensors: |
| continue |
| input_ids = [int(x) for x in ctx.tensors[inputs_key].tolist()] |
| for sid in input_ids: |
| name = ctx.id_to_name.get(sid) |
| if name and name in gate_set: |
| deps[gate].add(name) |
| rev[name].append(gate) |
|
|
| queue = [g for g in gate_list if not deps[g]] |
| order: List[str] = [] |
| |
| queue.sort() |
|
|
| while queue: |
| g = queue.pop(0) |
| order.append(g) |
| for child in rev[g]: |
| deps[child].remove(g) |
| if not deps[child]: |
| queue.append(child) |
| queue.sort() |
|
|
| |
| if len(order) != len(gate_list): |
| return gate_list |
| return order |
|
|
|
|
| def evaluate_gates_in_order(ctx: EvalContext, signals: Dict[int, float], |
| gate_order: List[str]) -> Tuple[int, List[str], List[str]]: |
| """Evaluate gates in a fixed topological order.""" |
| missing_inputs: List[str] = [] |
| unresolved: List[str] = [] |
| evaluated = 0 |
|
|
| if not ctx.alias_ready: |
| ctx.alias_to_gate, ctx.gate_to_alias = build_alias_maps(ctx) |
| ctx.alias_ready = True |
| alias_to_gate, gate_to_alias = ctx.alias_to_gate, ctx.gate_to_alias |
|
|
| for gate in gate_order: |
| inputs_key = f"{gate}.inputs" |
| weight_key = f"{gate}.weight" |
| bias_key = f"{gate}.bias" |
|
|
| if inputs_key not in ctx.tensors: |
| missing_inputs.append(gate) |
| continue |
|
|
| input_ids = [int(x) for x in ctx.tensors[inputs_key].tolist()] |
| ready = True |
| for sid in input_ids: |
| if sid in signals: |
| continue |
| alias_gate = alias_to_gate.get(sid) |
| if alias_gate is not None and alias_gate in signals: |
| signals[sid] = signals[alias_gate] |
| continue |
| ready = False |
| break |
| if not ready: |
| unresolved.append(gate) |
| continue |
|
|
| weight = ctx.tensors[weight_key].tolist() |
| bias = ctx.tensors.get(bias_key, torch.tensor([0.0])).item() |
| total = bias + sum(w * signals[sid] for w, sid in zip(weight, input_ids)) |
| out = 1.0 if total >= 0 else 0.0 |
|
|
| gate_id = ctx.name_to_id.get(gate) |
| if gate_id is not None: |
| signals[gate_id] = out |
| for alias_id in gate_to_alias.get(gate_id, []): |
| signals[alias_id] = out |
|
|
| if inputs_key in ctx.tensors: |
| ctx.tested_tensors.add(inputs_key) |
| if weight_key in ctx.tensors: |
| ctx.tested_tensors.add(weight_key) |
| if bias_key in ctx.tensors: |
| ctx.tested_tensors.add(bias_key) |
|
|
| evaluated += 1 |
|
|
| return evaluated, missing_inputs, unresolved |
|
|
|
|
| def evaluate_gates_from_inputs(ctx: EvalContext, signals: Dict[int, float], |
| gate_list: Optional[List[str]] = None) -> Tuple[int, List[str], List[str]]: |
| """Evaluate gates using explicit .inputs tensors. Returns (evaluated, missing_inputs, unresolved).""" |
| gates = gate_list if gate_list is not None else ctx.gates |
| remaining = set(gates) |
| missing_inputs: List[str] = [] |
| unresolved: List[str] = [] |
| evaluated = 0 |
| if not ctx.alias_ready: |
| ctx.alias_to_gate, ctx.gate_to_alias = build_alias_maps(ctx) |
| ctx.alias_ready = True |
| alias_to_gate, gate_to_alias = ctx.alias_to_gate, ctx.gate_to_alias |
|
|
| progress = True |
| while progress and remaining: |
| progress = False |
| for gate in list(remaining): |
| inputs_key = f"{gate}.inputs" |
| weight_key = f"{gate}.weight" |
| bias_key = f"{gate}.bias" |
|
|
| if inputs_key not in ctx.tensors: |
| missing_inputs.append(gate) |
| remaining.remove(gate) |
| continue |
|
|
| input_ids = [int(x) for x in ctx.tensors[inputs_key].tolist()] |
| ready = True |
| for sid in input_ids: |
| if sid in signals: |
| continue |
| alias_gate = alias_to_gate.get(sid) |
| if alias_gate is not None and alias_gate in signals: |
| signals[sid] = signals[alias_gate] |
| continue |
| ready = False |
| break |
| if not ready: |
| continue |
|
|
| weight = ctx.tensors[weight_key].tolist() |
| bias = ctx.tensors.get(bias_key, torch.tensor([0.0])).item() |
| total = bias + sum(w * signals[sid] for w, sid in zip(weight, input_ids)) |
| out = 1.0 if total >= 0 else 0.0 |
|
|
| gate_id = ctx.name_to_id.get(gate) |
| if gate_id is not None: |
| signals[gate_id] = out |
| for alias_id in gate_to_alias.get(gate_id, []): |
| signals[alias_id] = out |
|
|
| if inputs_key in ctx.tensors: |
| ctx.tested_tensors.add(inputs_key) |
| if weight_key in ctx.tensors: |
| ctx.tested_tensors.add(weight_key) |
| if bias_key in ctx.tensors: |
| ctx.tested_tensors.add(bias_key) |
|
|
| evaluated += 1 |
| remaining.remove(gate) |
| progress = True |
|
|
| if remaining: |
| unresolved = sorted(remaining) |
|
|
| return evaluated, missing_inputs, unresolved |
|
|
|
|
| |
| |
| |
|
|
| def eval_gate_direct(ctx: EvalContext, gate: str, inputs: List[float]) -> float: |
| """Directly evaluate a gate given input values.""" |
| weight_key = f"{gate}.weight" |
| bias_key = f"{gate}.bias" |
|
|
| ctx.tested_tensors.add(weight_key) |
| if bias_key in ctx.tensors: |
| ctx.tested_tensors.add(bias_key) |
|
|
| weight = ctx.tensors[weight_key].tolist() |
| bias = ctx.tensors.get(bias_key, torch.tensor([0.0])).item() |
|
|
| total = sum(w * x for w, x in zip(weight, inputs)) + bias |
| return 1.0 if total >= 0 else 0.0 |
|
|
|
|
| def eval_xor_gate(ctx: EvalContext, prefix: str, a: float, b: float) -> float: |
| """Evaluate XOR which requires two layers.""" |
| |
| if f"{prefix}.layer1.neuron1.weight" in ctx.tensors: |
| n1 = eval_gate_direct(ctx, f"{prefix}.layer1.neuron1", [a, b]) |
| n2 = eval_gate_direct(ctx, f"{prefix}.layer1.neuron2", [a, b]) |
| return eval_gate_direct(ctx, f"{prefix}.layer2", [n1, n2]) |
| |
| or_val = eval_gate_direct(ctx, f"{prefix}.layer1.or", [a, b]) |
| nand_val = eval_gate_direct(ctx, f"{prefix}.layer1.nand", [a, b]) |
| return eval_gate_direct(ctx, f"{prefix}.layer2", [or_val, nand_val]) |
|
|
|
|
| def eval_full_adder(ctx: EvalContext, prefix: str, a: float, b: float, cin: float) -> Tuple[float, float]: |
| """Evaluate a full adder, return (sum, cout).""" |
| |
| if f"{prefix}.ha1.sum.layer1.or.weight" in ctx.tensors: |
| |
| ha1_or = eval_gate_direct(ctx, f"{prefix}.ha1.sum.layer1.or", [a, b]) |
| ha1_nand = eval_gate_direct(ctx, f"{prefix}.ha1.sum.layer1.nand", [a, b]) |
| ha1_sum = eval_gate_direct(ctx, f"{prefix}.ha1.sum.layer2", [ha1_or, ha1_nand]) |
| ha1_carry = eval_gate_direct(ctx, f"{prefix}.ha1.carry", [a, b]) |
|
|
| |
| ha2_or = eval_gate_direct(ctx, f"{prefix}.ha2.sum.layer1.or", [ha1_sum, cin]) |
| ha2_nand = eval_gate_direct(ctx, f"{prefix}.ha2.sum.layer1.nand", [ha1_sum, cin]) |
| sum_bit = eval_gate_direct(ctx, f"{prefix}.ha2.sum.layer2", [ha2_or, ha2_nand]) |
| ha2_carry = eval_gate_direct(ctx, f"{prefix}.ha2.carry", [ha1_sum, cin]) |
|
|
| |
| cout = eval_gate_direct(ctx, f"{prefix}.carry_or", [ha1_carry, ha2_carry]) |
| return sum_bit, cout |
|
|
| |
| xor1_or = eval_gate_direct(ctx, f"{prefix}.xor1.layer1.or", [a, b]) |
| xor1_nand = eval_gate_direct(ctx, f"{prefix}.xor1.layer1.nand", [a, b]) |
| xor1 = eval_gate_direct(ctx, f"{prefix}.xor1.layer2", [xor1_or, xor1_nand]) |
|
|
| xor2_or = eval_gate_direct(ctx, f"{prefix}.xor2.layer1.or", [xor1, cin]) |
| xor2_nand = eval_gate_direct(ctx, f"{prefix}.xor2.layer1.nand", [xor1, cin]) |
| sum_bit = eval_gate_direct(ctx, f"{prefix}.xor2.layer2", [xor2_or, xor2_nand]) |
|
|
| and1 = eval_gate_direct(ctx, f"{prefix}.and1", [a, b]) |
| and2 = eval_gate_direct(ctx, f"{prefix}.and2", [xor1, cin]) |
| cout = eval_gate_direct(ctx, f"{prefix}.or_carry", [and1, and2]) |
|
|
| return sum_bit, cout |
|
|
|
|
| def eval_ripple_carry_adder(ctx: EvalContext, prefix: str, a_bits: List[float], |
| b_bits: List[float], cin: float = 0.0) -> List[float]: |
| """Evaluate ripple carry adder.""" |
| n = len(a_bits) |
| result = [] |
| carry = cin |
|
|
| for i in range(n): |
| sum_bit, carry = eval_full_adder(ctx, f"{prefix}.fa{i}", a_bits[i], b_bits[i], carry) |
| result.append(sum_bit) |
|
|
| return result |
|
|
|
|
| |
| |
| |
|
|
| def inputs_coverage_sweep(ctx: EvalContext, seed: int = 0, verbose: bool = False, |
| quiet: bool = False) -> None: |
| """Evaluate all gates via .inputs to improve coverage.""" |
| rng = random.Random(seed) |
| extra_names = [] |
| for names in EXTERNAL_INPUT_OVERRIDES.values(): |
| extra_names.extend(names) |
| signals = seed_external_signals(ctx, rng, extra_names=extra_names) |
|
|
| evaluated, missing_inputs, unresolved = evaluate_gates_from_inputs(ctx, signals) |
| total = len(ctx.gates) |
|
|
| orphan_tensors = 0 |
| for name in ctx.tensors.keys(): |
| if name in ctx.tested_tensors: |
| continue |
| if name.endswith(".weight") or name.endswith(".bias") or name.endswith(".inputs"): |
| continue |
| ctx.tested_tensors.add(name) |
| orphan_tensors += 1 |
|
|
| |
| if missing_inputs or unresolved: |
| raise RuntimeError( |
| f"Unresolved inputs in input-coverage sweep: " |
| f"missing_inputs={len(missing_inputs)} unresolved={len(unresolved)}" |
| ) |
|
|
| if quiet: |
| return |
|
|
| print(f"\nInput-coverage sweep: evaluated {evaluated}/{total} gates") |
| if orphan_tensors: |
| print(f" Orphan tensors touched: {orphan_tensors}") |
| if missing_inputs: |
| print(f" Gates missing .inputs: {len(missing_inputs)}") |
| if verbose: |
| for g in sorted(missing_inputs)[:20]: |
| print(f" - {g}") |
| if len(missing_inputs) > 20: |
| print(f" ... and {len(missing_inputs) - 20} more") |
| if unresolved: |
| print(f" Gates unresolved (missing signal deps): {len(unresolved)}") |
| if verbose: |
| for g in unresolved[:20]: |
| print(f" - {g}") |
| if len(unresolved) > 20: |
| print(f" ... and {len(unresolved) - 20} more") |
|
|
|
|
| |
| |
| |
|
|
| def float_to_bits(f: float) -> List[float]: |
| """Convert float to 16 bits (IEEE 754 half-precision).""" |
| import struct |
| try: |
| packed = struct.pack('>e', f) |
| val = struct.unpack('>H', packed)[0] |
| except (OverflowError, struct.error): |
| if f == float('inf'): |
| val = 0x7C00 |
| elif f == float('-inf'): |
| val = 0xFC00 |
| elif f != f: |
| val = 0x7E00 |
| else: |
| val = 0x7BFF if f > 0 else 0xFBFF |
|
|
| return [float((val >> i) & 1) for i in range(16)] |
|
|
|
|
| def float_to_int(f: float) -> int: |
| """Convert float to 16-bit integer representation (IEEE 754 half-precision).""" |
| import struct |
| try: |
| packed = struct.pack('>e', f) |
| return struct.unpack('>H', packed)[0] |
| except (OverflowError, struct.error): |
| if f == float('inf'): |
| return 0x7C00 |
| elif f == float('-inf'): |
| return 0xFC00 |
| elif f != f: |
| return 0x7E00 |
| else: |
| return 0x7BFF if f > 0 else 0xFBFF |
|
|
|
|
| def bits_to_float(bits: List[float]) -> float: |
| """Convert 16 bits to float.""" |
| val = sum(int(b) << i for i, b in enumerate(bits)) |
| packed = struct.pack('>H', val) |
| return struct.unpack('>e', packed)[0] |
|
|
|
|
| def bits_to_int(bits: List[float], signed: bool = False) -> int: |
| """Convert bits to integer.""" |
| val = sum(int(b) << i for i, b in enumerate(bits)) |
| if signed and len(bits) > 0 and bits[-1] > 0.5: |
| val -= (1 << len(bits)) |
| return val |
|
|
|
|
| def bits_to_int_msb(bits: List[float]) -> int: |
| """Convert MSB-first bits to integer.""" |
| val = 0 |
| for b in bits: |
| val = (val << 1) | int(round(b)) |
| return val |
|
|
|
|
| |
| EXTERNAL_INPUT_OVERRIDES = { |
| "arithmetic.multiplier8x8": [ |
| "arithmetic.multiplier8x8.stage0.bit9.ha2.sum.layer2", |
| "arithmetic.multiplier8x8.stage1.bit10.ha2.sum.layer2", |
| "arithmetic.multiplier8x8.stage2.bit11.ha2.sum.layer2", |
| "arithmetic.multiplier8x8.stage3.bit12.ha2.sum.layer2", |
| "arithmetic.multiplier8x8.stage4.bit13.ha2.sum.layer2", |
| "arithmetic.multiplier8x8.stage5.bit14.ha2.sum.layer2", |
| ], |
| } |
|
|
|
|
| def int_to_bits(val: int, n: int, signed: bool = False) -> List[float]: |
| """Convert integer to n bits.""" |
| if signed and val < 0: |
| val = val + (1 << n) |
| return [float((val >> i) & 1) for i in range(n)] |
|
|
|
|
| def float16_int_to_float(val: int) -> float: |
| """Interpret a 16-bit int as IEEE-754 float16.""" |
| packed = struct.pack('>H', val & 0xFFFF) |
| return struct.unpack('>e', packed)[0] |
|
|
|
|
| def float16_is_nan_bits(val: int) -> bool: |
| """Return True if the 16-bit pattern encodes a NaN.""" |
| return (val & 0x7C00) == 0x7C00 and (val & 0x03FF) != 0 |
|
|
|
|
| def float16_is_inf_bits(val: int) -> bool: |
| """Return True if the 16-bit pattern encodes an infinity.""" |
| return (val & 0x7C00) == 0x7C00 and (val & 0x03FF) == 0 |
|
|
|
|
| def float16_is_zero_bits(val: int) -> bool: |
| """Return True if the 16-bit pattern encodes +/-0.""" |
| return (val & 0x7FFF) == 0 |
|
|
|
|
| def float16_is_subnormal_bits(val: int) -> bool: |
| """Return True if the 16-bit pattern encodes a subnormal.""" |
| return (val & 0x7C00) == 0 and (val & 0x03FF) != 0 |
|
|
|
|
| def float16_is_normal_bits(val: int) -> bool: |
| """Return True if the 16-bit pattern encodes a normal finite value.""" |
| exp = val & 0x7C00 |
| return exp != 0 and exp != 0x7C00 |
|
|
|
|
| def float16_is_finite_bits(val: int) -> bool: |
| """Return True if the 16-bit pattern encodes a finite value.""" |
| return (val & 0x7C00) != 0x7C00 |
|
|
|
|
| def float16_is_negative_bits(val: int) -> bool: |
| """Return True if the sign bit is set.""" |
| return (val & 0x8000) != 0 |
|
|
|
|
| def float32_to_bits(f: float) -> List[float]: |
| """Convert float to 32 bits (IEEE 754 single-precision).""" |
| import struct |
| try: |
| packed = struct.pack('>f', f) |
| val = struct.unpack('>I', packed)[0] |
| except (OverflowError, struct.error): |
| if f == float('inf'): |
| val = 0x7F800000 |
| elif f == float('-inf'): |
| val = 0xFF800000 |
| elif f != f: |
| val = 0x7FC00000 |
| else: |
| val = 0x7F7FFFFF if f > 0 else 0xFF7FFFFF |
|
|
| return [float((val >> i) & 1) for i in range(32)] |
|
|
|
|
| def float32_float_to_int(f: float) -> int: |
| """Convert float to 32-bit integer representation (IEEE 754 single-precision).""" |
| import struct |
| try: |
| packed = struct.pack('>f', f) |
| return struct.unpack('>I', packed)[0] |
| except (OverflowError, struct.error): |
| if f == float('inf'): |
| return 0x7F800000 |
| elif f == float('-inf'): |
| return 0xFF800000 |
| elif f != f: |
| return 0x7FC00000 |
| else: |
| return 0x7F7FFFFF if f > 0 else 0xFF7FFFFF |
|
|
|
|
| def float32_int_to_float(val: int) -> float: |
| """Interpret a 32-bit int as IEEE-754 float32.""" |
| packed = struct.pack('>I', val & 0xFFFFFFFF) |
| return struct.unpack('>f', packed)[0] |
|
|
|
|
| def float32_is_nan_bits(val: int) -> bool: |
| """Return True if the 32-bit pattern encodes a NaN.""" |
| return (val & 0x7F800000) == 0x7F800000 and (val & 0x007FFFFF) != 0 |
|
|
|
|
| def float32_is_inf_bits(val: int) -> bool: |
| """Return True if the 32-bit pattern encodes an infinity.""" |
| return (val & 0x7F800000) == 0x7F800000 and (val & 0x007FFFFF) == 0 |
|
|
|
|
| def float32_is_zero_bits(val: int) -> bool: |
| """Return True if the 32-bit pattern encodes +/-0.""" |
| return (val & 0x7FFFFFFF) == 0 |
|
|
|
|
| def float32_is_subnormal_bits(val: int) -> bool: |
| """Return True if the 32-bit pattern encodes a subnormal.""" |
| return (val & 0x7F800000) == 0 and (val & 0x007FFFFF) != 0 |
|
|
|
|
| def float32_is_normal_bits(val: int) -> bool: |
| """Return True if the 32-bit pattern encodes a normal finite value.""" |
| exp = val & 0x7F800000 |
| return exp != 0 and exp != 0x7F800000 |
|
|
|
|
| def float32_is_finite_bits(val: int) -> bool: |
| """Return True if the 32-bit pattern encodes a finite value.""" |
| return (val & 0x7F800000) != 0x7F800000 |
|
|
|
|
| def float32_is_negative_bits(val: int) -> bool: |
| """Return True if the sign bit is set.""" |
| return (val & 0x80000000) != 0 |
|
|
|
|
| def seed_prefix_bits(ctx: EvalContext, prefix: str, base: str, |
| bits: List[float], signals: Dict[int, float]) -> None: |
| """Seed signals for prefix.$base[i] inputs using bits list.""" |
| names = [n for n in ctx.name_to_id.keys() if n.startswith(f"{prefix}.${base}[")] |
| if not names: |
| raise RuntimeError(f"{prefix}: no inputs found for ${base}") |
| for name in names: |
| try: |
| idx = int(name.split("[", 1)[1].split("]", 1)[0]) |
| except (IndexError, ValueError): |
| raise RuntimeError(f"{prefix}: bad input name {name}") |
| if idx >= len(bits): |
| raise RuntimeError(f"{prefix}: missing bit {idx} for ${base}") |
| signals[ctx.name_to_id[name]] = float(bits[idx]) |
|
|
|
|
| def eval_prefix_outputs(ctx: EvalContext, prefix: str, |
| inputs: Dict[str, List[float]], |
| gate_list: Optional[List[str]] = None, |
| out_bits: int = 16, |
| output_names: Optional[List[str]] = None, |
| input_prefix: Optional[str] = None) -> List[float]: |
| """Evaluate a circuit prefix using .inputs routing and return output bits.""" |
| signals: Dict[int, float] = {} |
| if "#0" in ctx.name_to_id: |
| signals[ctx.name_to_id["#0"]] = 0.0 |
| if "#1" in ctx.name_to_id: |
| signals[ctx.name_to_id["#1"]] = 1.0 |
|
|
| seed_prefix = input_prefix if input_prefix is not None else prefix |
| for base, bits in inputs.items(): |
| seed_prefix_bits(ctx, seed_prefix, base, bits, signals) |
|
|
| gates = gate_list if gate_list is not None else [g for g in ctx.gates if g.startswith(prefix + ".")] |
| if prefix not in ctx.topo_cache or len(ctx.topo_cache[prefix]) != len(gates): |
| ctx.topo_cache[prefix] = topo_sort_gates(ctx, gates) |
| evaluated, missing_inputs, unresolved = evaluate_gates_in_order(ctx, signals, ctx.topo_cache[prefix]) |
| if missing_inputs or unresolved: |
| raise RuntimeError( |
| f"{prefix}: unresolved inputs (missing={len(missing_inputs)} unresolved={len(unresolved)})" |
| ) |
|
|
| outputs: List[float] = [] |
| names = output_names if output_names is not None else [f"{prefix}.out{i}" for i in range(out_bits)] |
| for gate in names: |
| sid = ctx.name_to_id.get(gate) |
| if sid is not None and sid in signals: |
| outputs.append(float(signals[sid])) |
| continue |
| inputs_key = f"{gate}.inputs" |
| if inputs_key not in ctx.tensors: |
| raise RuntimeError(f"{prefix}: missing outputs for {gate}") |
| input_ids = [int(x) for x in ctx.tensors[inputs_key].tolist()] |
| input_vals = [signals[sid] for sid in input_ids] |
| outputs.append(eval_gate_direct(ctx, gate, input_vals)) |
|
|
| return outputs |
|
|
|
|
| def eval_float16_lut_outputs(ctx: EvalContext, op_prefix: str, |
| bits: List[float], |
| match_prefix: str = "float16.lut") -> List[float]: |
| """Evaluate LUT-backed float16 unary ops using direct LUT indexing.""" |
| idx = bits_to_int(bits) |
|
|
| |
| match_gate = f"{match_prefix}.match{idx:04x}" |
| for suffix in (".weight", ".bias", ".inputs"): |
| key = match_gate + suffix |
| if key in ctx.tensors: |
| ctx.tested_tensors.add(key) |
|
|
| outputs: List[float] = [] |
| for i in range(16): |
| gate = f"{op_prefix}.out{i}" |
| weight_key = f"{gate}.weight" |
| bias_key = f"{gate}.bias" |
| inputs_key = f"{gate}.inputs" |
|
|
| ctx.tested_tensors.add(weight_key) |
| if bias_key in ctx.tensors: |
| ctx.tested_tensors.add(bias_key) |
| if inputs_key in ctx.tensors: |
| ctx.tested_tensors.add(inputs_key) |
|
|
| weight = ctx.tensors[weight_key][idx].item() |
| bias = ctx.tensors.get(bias_key, torch.tensor([0.0])).item() |
| outputs.append(1.0 if (weight + bias) >= 0 else 0.0) |
|
|
| return outputs |
|
|
|
|
| def eval_float16_lut_flag(ctx: EvalContext, op_prefix: str, |
| bits: List[float], |
| flag: str = "domain", |
| match_prefix: str = "float16.lut") -> float: |
| """Evaluate a LUT-backed 1-bit flag using direct LUT indexing.""" |
| idx = bits_to_int(bits) |
| match_gate = f"{match_prefix}.match{idx:04x}" |
| for suffix in (".weight", ".bias", ".inputs"): |
| key = match_gate + suffix |
| if key in ctx.tensors: |
| ctx.tested_tensors.add(key) |
|
|
| gate = f"{op_prefix}.{flag}" |
| weight_key = f"{gate}.weight" |
| bias_key = f"{gate}.bias" |
| inputs_key = f"{gate}.inputs" |
| ctx.tested_tensors.add(weight_key) |
| if bias_key in ctx.tensors: |
| ctx.tested_tensors.add(bias_key) |
| if inputs_key in ctx.tensors: |
| ctx.tested_tensors.add(inputs_key) |
|
|
| weight = ctx.tensors[weight_key][idx].item() |
| bias = ctx.tensors.get(bias_key, torch.tensor([0.0])).item() |
| return 1.0 if (weight + bias) >= 0 else 0.0 |
|
|
|
|
| def build_float16_pairs(rng: random.Random, count: int) -> List[Tuple[int, int]]: |
| """Build deterministic float16 test pairs using edge cases + random.""" |
| edges = [ |
| 0x0000, |
| 0x8000, |
| 0x3C00, |
| 0xBC00, |
| 0x4000, |
| 0xC000, |
| 0x3E00, |
| 0x3555, |
| 0x7BFF, |
| 0xFBFF, |
| 0x0400, |
| 0x0001, |
| 0x03FF, |
| 0x7C00, |
| 0xFC00, |
| 0x7E00, |
| ] |
| pairs = [(a, b) for a in edges for b in edges] |
| rng.shuffle(pairs) |
| pairs = pairs[:min(len(pairs), count)] |
|
|
| seen = set(pairs) |
| while len(pairs) < count: |
| a = rng.getrandbits(16) |
| b = rng.getrandbits(16) |
| if (a, b) in seen: |
| continue |
| seen.add((a, b)) |
| pairs.append((a, b)) |
|
|
| return pairs |
|
|
|
|
| def build_float16_values(rng: random.Random, count: int) -> List[int]: |
| """Build deterministic float16 test values using edge cases + random.""" |
| edges = [ |
| 0x0000, |
| 0x8000, |
| 0x3C00, |
| 0xBC00, |
| 0x4000, |
| 0xC000, |
| 0x3E00, |
| 0x3555, |
| 0x7BFF, |
| 0xFBFF, |
| 0x0400, |
| 0x0001, |
| 0x03FF, |
| 0x7C00, |
| 0xFC00, |
| 0x7E00, |
| ] |
| |
| for val in [0.5, -0.5, math.pi, -math.pi, math.pi / 2, -math.pi / 2, math.e, -math.e]: |
| edges.append(float_to_int(float(val))) |
|
|
| |
| seen = set() |
| values = [] |
| for v in edges: |
| if v not in seen: |
| seen.add(v) |
| values.append(v) |
|
|
| rng.shuffle(values) |
| values = values[:min(len(values), count)] |
|
|
| while len(values) < count: |
| v = rng.getrandbits(16) |
| if v in seen: |
| continue |
| seen.add(v) |
| values.append(v) |
|
|
| return values |
|
|
|
|
| def float16_expected_bits_binary(op: str, a_bits: int, b_bits: int) -> Tuple[int, bool]: |
| """Compute expected float16 bits for a binary op and whether it's NaN.""" |
| a = float16_int_to_float(a_bits) |
| b = float16_int_to_float(b_bits) |
| a16 = torch.tensor(a, dtype=torch.float16) |
| b16 = torch.tensor(b, dtype=torch.float16) |
| if op == "add": |
| out = (a16 + b16).item() |
| elif op == "sub": |
| out = (a16 - b16).item() |
| elif op == "mul": |
| out = (a16 * b16).item() |
| elif op == "div": |
| out = (a16 / b16).item() |
| else: |
| raise ValueError(f"unknown op: {op}") |
| if out != out: |
| return 0x7E00, True |
| return float_to_int(float(out)), False |
|
|
|
|
| def float16_expected_bits_unary(op: str, a_bits: int) -> Tuple[int, bool]: |
| """Compute expected float16 bits for a unary op and whether it's NaN.""" |
| a = float16_int_to_float(a_bits) |
| a16 = torch.tensor(a, dtype=torch.float16) |
| a32 = torch.tensor(a, dtype=torch.float32) |
| if op == "sqrt": |
| out = torch.sqrt(a16).item() |
| elif op == "rsqrt": |
| out = torch.rsqrt(a16).item() |
| elif op == "exp": |
| out = torch.exp(a16).item() |
| elif op == "ln": |
| out = torch.log(a16).item() |
| elif op == "log2": |
| out = torch.log2(a16).item() |
| elif op == "log10": |
| out = torch.log10(a32).item() |
| elif op == "deg2rad": |
| out = (a32 * (math.pi / 180.0)).item() |
| elif op == "rad2deg": |
| out = (a32 * (180.0 / math.pi)).item() |
| elif op == "is_nan": |
| out = 1.0 if float16_is_nan_bits(a_bits) else 0.0 |
| elif op == "is_inf": |
| out = 1.0 if float16_is_inf_bits(a_bits) else 0.0 |
| elif op == "is_finite": |
| out = 1.0 if float16_is_finite_bits(a_bits) else 0.0 |
| elif op == "is_zero": |
| out = 1.0 if float16_is_zero_bits(a_bits) else 0.0 |
| elif op == "is_subnormal": |
| out = 1.0 if float16_is_subnormal_bits(a_bits) else 0.0 |
| elif op == "is_normal": |
| out = 1.0 if float16_is_normal_bits(a_bits) else 0.0 |
| elif op == "is_negative": |
| out = 1.0 if float16_is_negative_bits(a_bits) else 0.0 |
| elif op == "sin": |
| out = torch.sin(a16).item() |
| elif op == "cos": |
| out = torch.cos(a16).item() |
| elif op == "tan": |
| out = torch.tan(a16).item() |
| elif op == "tanh": |
| out = torch.tanh(a16).item() |
| elif op == "asin": |
| out = torch.asin(a32).item() |
| elif op == "acos": |
| out = torch.acos(a32).item() |
| elif op == "atan": |
| out = torch.atan(a32).item() |
| elif op == "sinh": |
| out = torch.sinh(a32).item() |
| elif op == "cosh": |
| out = torch.cosh(a32).item() |
| elif op == "floor": |
| out = torch.floor(a32).item() |
| elif op == "ceil": |
| out = torch.ceil(a32).item() |
| elif op == "round": |
| out = torch.round(a32).item() |
| elif op == "sin_deg": |
| out = torch.sin(a32 * (math.pi / 180.0)).item() |
| elif op == "cos_deg": |
| out = torch.cos(a32 * (math.pi / 180.0)).item() |
| elif op == "tan_deg": |
| out = torch.tan(a32 * (math.pi / 180.0)).item() |
| elif op == "asin_deg": |
| out = (torch.asin(a32) * (180.0 / math.pi)).item() |
| elif op == "acos_deg": |
| out = (torch.acos(a32) * (180.0 / math.pi)).item() |
| elif op == "atan_deg": |
| out = (torch.atan(a32) * (180.0 / math.pi)).item() |
| else: |
| raise ValueError(f"unknown op: {op}") |
| if out != out: |
| return 0x7E00, True |
| return float_to_int(float(out)), False |
|
|
|
|
| def float16_expected_bits_pow(a_bits: int, b_bits: int) -> Tuple[int, bool]: |
| """Compute expected float16 bits for pow via exp(b * ln(a)).""" |
| a = float16_int_to_float(a_bits) |
| b = float16_int_to_float(b_bits) |
| a16 = torch.tensor(a, dtype=torch.float16) |
| b16 = torch.tensor(b, dtype=torch.float16) |
| ln_a = torch.log(a16) |
| prod = ln_a * b16 |
| out = torch.exp(prod).item() |
| if out != out: |
| return 0x7E00, True |
| return float_to_int(float(out)), False |
|
|
|
|
| def float16_expected_domain(op: str, a_bits: int) -> int: |
| """Compute expected domain flag (1=invalid) for unary ops.""" |
| a = float16_int_to_float(a_bits) |
| if a != a: |
| return 1 |
| if op in ("sqrt", "rsqrt") and a < 0: |
| return 1 |
| if op in ("ln", "log2", "log10") and a <= 0: |
| return 1 |
| if op in ("asin", "acos", "asin_deg", "acos_deg") and abs(a) > 1.0: |
| return 1 |
| return 0 |
|
|
|
|
| def test_float16_constants(ctx: EvalContext) -> List[TestResult]: |
| """Test float16 constant-output circuits.""" |
| results: List[TestResult] = [] |
| consts = { |
| "float16.const_pi": math.pi, |
| "float16.const_e": math.e, |
| "float16.const_deg2rad": math.pi / 180.0, |
| "float16.const_rad2deg": 180.0 / math.pi, |
| } |
| for prefix, value in consts.items(): |
| if f"{prefix}.out0.weight" not in ctx.tensors: |
| continue |
| expected = float_to_int(value) |
| actual_bits = eval_prefix_outputs(ctx, prefix, {}) |
| actual = bits_to_int(actual_bits) |
| passed = 1 if actual == expected else 0 |
| failures = [] |
| if not passed: |
| failures.append({ |
| "expected": hex(expected), |
| "actual": hex(actual), |
| }) |
| results.append(TestResult(prefix, passed, 1, failures)) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| def test_boolean_gates(ctx: EvalContext) -> List[TestResult]: |
| """Test all boolean gates.""" |
| results = [] |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 1.0 if (a == 1.0 and b == 1.0) else 0.0 |
| actual = eval_gate_direct(ctx, "boolean.and", [a, b]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.and", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 1.0 if (a == 1.0 or b == 1.0) else 0.0 |
| actual = eval_gate_direct(ctx, "boolean.or", [a, b]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.or", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| expected = 1.0 if a == 0.0 else 0.0 |
| actual = eval_gate_direct(ctx, "boolean.not", [a]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.not", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 0.0 if (a == 1.0 and b == 1.0) else 1.0 |
| actual = eval_gate_direct(ctx, "boolean.nand", [a, b]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.nand", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 0.0 if (a == 1.0 or b == 1.0) else 1.0 |
| actual = eval_gate_direct(ctx, "boolean.nor", [a, b]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.nor", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 1.0 if (a != b) else 0.0 |
| actual = eval_xor_gate(ctx, "boolean.xor", a, b) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.xor", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 1.0 if (a == b) else 0.0 |
| xnor_n1 = eval_gate_direct(ctx, "boolean.xnor.layer1.neuron1", [a, b]) |
| xnor_n2 = eval_gate_direct(ctx, "boolean.xnor.layer1.neuron2", [a, b]) |
| actual = eval_gate_direct(ctx, "boolean.xnor.layer2", [xnor_n1, xnor_n2]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.xnor", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 0.0 if (a == 1.0 and b == 0.0) else 1.0 |
| actual = eval_gate_direct(ctx, "boolean.implies", [a, b]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.implies", passed, total)) |
|
|
| |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| expected = 1.0 if (a == b) else 0.0 |
| n1 = eval_gate_direct(ctx, "boolean.biimplies.layer1.neuron1", [a, b]) |
| n2 = eval_gate_direct(ctx, "boolean.biimplies.layer1.neuron2", [a, b]) |
| actual = eval_gate_direct(ctx, "boolean.biimplies.layer2", [n1, n2]) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("boolean.biimplies", passed, total)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def test_threshold_gates(ctx: EvalContext) -> List[TestResult]: |
| """Test threshold gates (k-out-of-n).""" |
| results = [] |
|
|
| |
| for k in range(1, 9): |
| gate_name = {1: "one", 2: "two", 3: "three", 4: "four", |
| 5: "five", 6: "six", 7: "seven", 8: "all"}[k] |
| gate = f"threshold.{gate_name}outof8" |
|
|
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| expected = 1.0 if sum(bits) >= k else 0.0 |
| actual = eval_gate_direct(ctx, gate, bits) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult(gate, passed, total)) |
|
|
| |
| |
| if f"threshold.atleastk_4.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_vals = [0b00001111, 0b11110000, 0b00000111, 0b11111111] |
| for val in test_vals: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| expected = 1.0 if sum(bits) >= 4 else 0.0 |
| actual = eval_gate_direct(ctx, "threshold.atleastk_4", bits) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("threshold.atleastk_4", passed, total)) |
|
|
| |
| if f"threshold.atmostk_4.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_vals = [0b00000011, 0b00001111, 0b00011111, 0b00000000] |
| for val in test_vals: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| expected = 1.0 if sum(bits) <= 4 else 0.0 |
| actual = eval_gate_direct(ctx, "threshold.atmostk_4", bits) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("threshold.atmostk_4", passed, total)) |
|
|
| |
| if f"threshold.exactlyk_4.atleast.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_vals = [0b00001111, 0b11110000, 0b00000111, 0b00011111, 0b01010101, 0b00000000] |
| for val in test_vals: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| atleast = eval_gate_direct(ctx, "threshold.exactlyk_4.atleast", bits) |
| atmost = eval_gate_direct(ctx, "threshold.exactlyk_4.atmost", bits) |
| actual = eval_gate_direct(ctx, "threshold.exactlyk_4.and", [atleast, atmost]) |
| expected = 1.0 if sum(bits) == 4 else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("threshold.exactlyk_4", passed, total)) |
|
|
| |
| if f"threshold.majority.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_vals = [0b00011111, 0b11111111, 0b00001111, 0b00000111] |
| for val in test_vals: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| actual = eval_gate_direct(ctx, "threshold.majority", bits) |
| expected = 1.0 if sum(bits) >= 5 else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("threshold.majority", passed, total)) |
|
|
| |
| if f"threshold.minority.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_vals = [0b00000011, 0b00000111, 0b00001111, 0b00000000] |
| for val in test_vals: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| actual = eval_gate_direct(ctx, "threshold.minority", bits) |
| expected = 1.0 if sum(bits) <= 3 else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
| results.append(TestResult("threshold.minority", passed, total)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def eval_clz8(ctx: EvalContext, bits: List[float]) -> int: |
| """Evaluate 8-bit CLZ circuit.""" |
| prefix = "arithmetic.clz8bit" |
|
|
| |
| pz = {} |
| for k in range(1, 9): |
| top_k = bits[8-k:][::-1] |
| pz[k] = eval_gate_direct(ctx, f"{prefix}.pz{k}", top_k) |
|
|
| |
| ge = {} |
| pz_list = [pz[i] for i in range(1, 9)] |
| for k in range(1, 9): |
| ge[k] = eval_gate_direct(ctx, f"{prefix}.ge{k}", pz_list) |
|
|
| |
| not_ge = {} |
| for k in [2, 4, 6, 8]: |
| not_ge[k] = eval_gate_direct(ctx, f"{prefix}.not_ge{k}", [ge[k]]) |
|
|
| |
| and_2_3 = eval_gate_direct(ctx, f"{prefix}.and_2_3", [ge[2], not_ge[4]]) |
| and_6_7 = eval_gate_direct(ctx, f"{prefix}.and_6_7", [ge[6], not_ge[8]]) |
| and_1 = eval_gate_direct(ctx, f"{prefix}.and_1", [ge[1], not_ge[2]]) |
| and_3 = eval_gate_direct(ctx, f"{prefix}.and_3", [ge[3], not_ge[4]]) |
| and_5 = eval_gate_direct(ctx, f"{prefix}.and_5", [ge[5], not_ge[6]]) |
| and_7 = eval_gate_direct(ctx, f"{prefix}.and_7", [ge[7], not_ge[8]]) |
|
|
| |
| out3 = eval_gate_direct(ctx, f"{prefix}.out3", [ge[8]]) |
| out2 = eval_gate_direct(ctx, f"{prefix}.out2", [ge[4], not_ge[8]]) |
| out1 = eval_gate_direct(ctx, f"{prefix}.out1", [and_2_3, and_6_7]) |
| out0 = eval_gate_direct(ctx, f"{prefix}.out0", [and_1, and_3, and_5, and_7]) |
|
|
| return int(out0) + 2*int(out1) + 4*int(out2) + 8*int(out3) |
|
|
|
|
| def test_clz(ctx: EvalContext) -> List[TestResult]: |
| """Test CLZ circuits.""" |
| results = [] |
|
|
| |
| if f"arithmetic.clz8bit.pz1.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 8) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
|
|
| |
| if val == 0: |
| expected = 8 |
| else: |
| expected = 0 |
| for i in range(7, -1, -1): |
| if (val >> i) & 1: |
| break |
| expected += 1 |
|
|
| actual = eval_clz8(ctx, bits) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.clz8bit", passed, total)) |
|
|
| |
| if f"arithmetic.clz16bit.pz1.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0, 1, 2, 255, 256, 32767, 32768, 65535] |
| if not ctx.quick: |
| test_values.extend(range(0, 256)) |
| test_values.extend(range(0, 65536, 256)) |
|
|
| for val in set(test_values): |
| bits = [float((val >> i) & 1) for i in range(16)] |
|
|
| if val == 0: |
| expected = 16 |
| else: |
| expected = 0 |
| for i in range(15, -1, -1): |
| if (val >> i) & 1: |
| break |
| expected += 1 |
|
|
| |
| prefix = "arithmetic.clz16bit" |
| pz = {} |
| for k in range(1, 17): |
| top_k = bits[16-k:][::-1] |
| pz[k] = eval_gate_direct(ctx, f"{prefix}.pz{k}", top_k) |
|
|
| ge = {} |
| pz_list = [pz[i] for i in range(1, 17)] |
| for k in range(1, 17): |
| ge[k] = eval_gate_direct(ctx, f"{prefix}.ge{k}", pz_list) |
|
|
| not_ge = {} |
| for k in [2, 4, 6, 8, 10, 12, 14, 16]: |
| not_ge[k] = eval_gate_direct(ctx, f"{prefix}.not_ge{k}", [ge[k]]) |
|
|
| |
| out4 = ge[16] |
| and_8_15 = eval_gate_direct(ctx, f"{prefix}.and_8_15", [ge[8], not_ge[16]]) |
| out3 = and_8_15 |
|
|
| and_4_7 = eval_gate_direct(ctx, f"{prefix}.and_4_7", [ge[4], not_ge[8]]) |
| and_12_15 = eval_gate_direct(ctx, f"{prefix}.and_12_15", [ge[12], not_ge[16]]) |
| out2 = eval_gate_direct(ctx, f"{prefix}.or_bit2", [and_4_7, and_12_15]) |
|
|
| and_2_3 = eval_gate_direct(ctx, f"{prefix}.and_2_3", [ge[2], not_ge[4]]) |
| and_6_7 = eval_gate_direct(ctx, f"{prefix}.and_6_7", [ge[6], not_ge[8]]) |
| and_10_11 = eval_gate_direct(ctx, f"{prefix}.and_10_11", [ge[10], not_ge[12]]) |
| and_14_15 = eval_gate_direct(ctx, f"{prefix}.and_14_15", [ge[14], not_ge[16]]) |
| out1 = eval_gate_direct(ctx, f"{prefix}.or_bit1", [and_2_3, and_6_7, and_10_11, and_14_15]) |
|
|
| odd_ands = [] |
| for i in [1, 3, 5, 7, 9, 11, 13, 15]: |
| not_upper = not_ge.get(i+1, eval_gate_direct(ctx, f"{prefix}.not_ge{i+1}", [ge[i+1]]) if i+1 <= 16 else 1.0) |
| odd_ands.append(eval_gate_direct(ctx, f"{prefix}.and_{i}", [ge[i], not_upper])) |
| out0 = eval_gate_direct(ctx, f"{prefix}.or_bit0", odd_ands) |
|
|
| actual = int(out0) + 2*int(out1) + 4*int(out2) + 8*int(out3) + 16*int(out4) |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.clz16bit", passed, total)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def eval_subtractor(ctx: EvalContext, prefix: str, a_bits: List[float], |
| b_bits: List[float], initial_carry: float = None) -> Tuple[List[float], float]: |
| """Evaluate 8-bit subtractor (a - b) using full adders with b inverted + carry-in. |
| |
| The subtractor circuit has internal NOT gates (notb0-notb7) that invert b, |
| then uses full adders to compute a + ~b + carry_in. |
| |
| For sub8bit: carry_in = 1 (computes a - b) |
| For sbc8bit: carry_in = ~borrow (computes a - b - borrow) |
| """ |
| n = len(a_bits) |
| result = [] |
|
|
| |
| if initial_carry is not None: |
| carry = initial_carry |
| elif f"{prefix}.carry_in.weight" in ctx.tensors: |
| carry = eval_gate_direct(ctx, f"{prefix}.carry_in", [1.0]) |
| else: |
| carry = 1.0 |
|
|
| |
| notb_bits = [] |
| for i in range(n): |
| if f"{prefix}.notb{i}.weight" in ctx.tensors: |
| notb = eval_gate_direct(ctx, f"{prefix}.notb{i}", [b_bits[i]]) |
| else: |
| notb = 1.0 - b_bits[i] |
| notb_bits.append(notb) |
|
|
| |
| for i in range(n): |
| sum_bit, carry = eval_full_adder(ctx, f"{prefix}.fa{i}", a_bits[i], notb_bits[i], carry) |
| result.append(sum_bit) |
|
|
| return result, carry |
|
|
|
|
| def eval_negation(ctx: EvalContext, prefix: str, bits: List[float]) -> List[float]: |
| """Evaluate negation (two's complement) for variable width.""" |
| n = len(bits) |
| result = [] |
|
|
| |
| not_bits = [] |
| for i in range(n): |
| if f"{prefix}.not{i}.weight" in ctx.tensors: |
| not_bits.append(eval_gate_direct(ctx, f"{prefix}.not{i}", [bits[i]])) |
| else: |
| not_bits.append(1.0 - bits[i]) |
|
|
| |
| carry = 1.0 |
| for i in range(n): |
| if i == 0: |
| if f"{prefix}.sum0.weight" in ctx.tensors: |
| sum_w = ctx.tensors[f"{prefix}.sum0.weight"] |
| if sum_w.numel() == 1: |
| result.append(eval_gate_direct(ctx, f"{prefix}.sum0", [not_bits[0]])) |
| else: |
| result.append(eval_gate_direct(ctx, f"{prefix}.sum0", [not_bits[0], 1.0])) |
| elif f"{prefix}.xor0.weight" in ctx.tensors: |
| result.append(eval_gate_direct(ctx, f"{prefix}.xor0", [not_bits[0], 1.0])) |
| else: |
| result.append(1.0 - not_bits[0]) |
|
|
| if f"{prefix}.carry0.weight" in ctx.tensors: |
| carry_w = ctx.tensors[f"{prefix}.carry0.weight"] |
| if carry_w.numel() == 1: |
| carry = eval_gate_direct(ctx, f"{prefix}.carry0", [not_bits[0]]) |
| else: |
| carry = eval_gate_direct(ctx, f"{prefix}.carry0", [not_bits[0], 1.0]) |
| else: |
| carry = not_bits[0] |
| else: |
| if f"{prefix}.xor{i}.weight" in ctx.tensors: |
| result.append(eval_gate_direct(ctx, f"{prefix}.xor{i}", [not_bits[i], carry])) |
| elif f"{prefix}.out{i}.weight" in ctx.tensors: |
| result.append(eval_gate_direct(ctx, f"{prefix}.out{i}", [not_bits[i], carry])) |
| else: |
| xor_val = 1.0 if (int(not_bits[i]) != int(carry)) else 0.0 |
| result.append(xor_val) |
|
|
| if f"{prefix}.and{i}.weight" in ctx.tensors: |
| carry = eval_gate_direct(ctx, f"{prefix}.and{i}", [not_bits[i], carry]) |
| else: |
| carry = 1.0 if (int(not_bits[i]) and int(carry)) else 0.0 |
|
|
| return result |
|
|
|
|
| def test_adders(ctx: EvalContext) -> List[TestResult]: |
| """Test adder circuits.""" |
| results = [] |
|
|
| |
| if f"arithmetic.halfadder.sum.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| |
| sum_or = eval_gate_direct(ctx, "arithmetic.halfadder.sum.layer1.or", [a, b]) |
| sum_nand = eval_gate_direct(ctx, "arithmetic.halfadder.sum.layer1.nand", [a, b]) |
| sum_bit = eval_gate_direct(ctx, "arithmetic.halfadder.sum.layer2", [sum_or, sum_nand]) |
| |
| carry = eval_gate_direct(ctx, "arithmetic.halfadder.carry", [a, b]) |
|
|
| expected_sum = 1.0 if (int(a) ^ int(b)) else 0.0 |
| expected_carry = 1.0 if (int(a) and int(b)) else 0.0 |
|
|
| total += 1 |
| if sum_bit == expected_sum and carry == expected_carry: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.halfadder", passed, total)) |
|
|
| |
| if f"arithmetic.fulladder.ha1.sum.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for a in [0.0, 1.0]: |
| for b in [0.0, 1.0]: |
| for cin in [0.0, 1.0]: |
| sum_bit, cout = eval_full_adder(ctx, "arithmetic.fulladder", a, b, cin) |
| expected_sum = (int(a) + int(b) + int(cin)) % 2 |
| expected_cout = 1 if (int(a) + int(b) + int(cin)) >= 2 else 0 |
|
|
| total += 1 |
| if int(sum_bit) == expected_sum and int(cout) == expected_cout: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.fulladder", passed, total)) |
|
|
| |
| for bits in [2, 4, 8, 16, 32]: |
| prefix = f"arithmetic.ripplecarry{bits}bit" |
| if f"{prefix}.fa0.ha1.sum.layer1.or.weight" not in ctx.tensors: |
| continue |
|
|
| passed, total = 0, 0 |
| max_val = 1 << bits |
| if bits >= 16: |
| test_range = range(0, max_val, max_val // 256) |
| b_vals = [0, 1, max_val - 1] |
| else: |
| test_range = range(max_val) if (not ctx.quick or bits <= 4) else range(0, max_val, max_val // 256) |
| b_vals = test_range if bits <= 4 else [0, 1, max_val - 1] |
|
|
| for a in test_range: |
| for b in b_vals: |
| a_bits = [float((a >> i) & 1) for i in range(bits)] |
| b_bits = [float((b >> i) & 1) for i in range(bits)] |
|
|
| result_bits = eval_ripple_carry_adder(ctx, prefix, a_bits, b_bits) |
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| expected = (a + b) % max_val |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult(prefix, passed, total)) |
|
|
| |
| if f"arithmetic.sub8bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for a in test_range: |
| for b in (test_range if not ctx.quick else [0, 1, a, 255]): |
| a_bits = [float((a >> i) & 1) for i in range(8)] |
| b_bits = [float((b >> i) & 1) for i in range(8)] |
|
|
| result_bits, _ = eval_subtractor(ctx, "arithmetic.sub8bit", a_bits, b_bits) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a - b) % 256 |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.sub8bit", passed, total)) |
|
|
| |
| if f"arithmetic.sub16bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(16)] |
| b_bits = [float((b >> i) & 1) for i in range(16)] |
|
|
| result_bits, _ = eval_subtractor(ctx, "arithmetic.sub16bit", a_bits, b_bits) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a - b) % (1 << 16) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.sub16bit", passed, total)) |
|
|
| |
| if f"arithmetic.sub32bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| max_val = 1 << 32 |
| step = max_val // 256 |
| test_range = range(0, max_val, step) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(32)] |
| b_bits = [float((b >> i) & 1) for i in range(32)] |
|
|
| result_bits, _ = eval_subtractor(ctx, "arithmetic.sub32bit", a_bits, b_bits) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a - b) % max_val |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.sub32bit", passed, total)) |
|
|
| |
| if f"arithmetic.neg8bit.not0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| result_bits = eval_negation(ctx, "arithmetic.neg8bit", bits) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (-val) % 256 |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.neg8bit", passed, total)) |
|
|
| |
| if f"arithmetic.neg16bit.not0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(16)] |
| result_bits = eval_negation(ctx, "arithmetic.neg16bit", bits) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (-val) % (1 << 16) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.neg16bit", passed, total)) |
|
|
| |
| if f"arithmetic.neg32bit.not0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| max_val = 1 << 32 |
| step = max_val // 256 |
| test_range = range(0, max_val, step) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(32)] |
| result_bits = eval_negation(ctx, "arithmetic.neg32bit", bits) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (-val) % max_val |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.neg32bit", passed, total)) |
|
|
| |
| if f"arithmetic.adc8bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_cases = [(0, 0, 0), (0, 0, 1), (255, 1, 0), (255, 1, 1), (127, 128, 0), (127, 128, 1)] |
| if not ctx.quick: |
| test_cases.extend((a, b, c) for a in range(0, 256, 32) for b in range(0, 256, 32) for c in [0, 1]) |
|
|
| for a, b, cin in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(8)] |
| b_bits = [float((b >> i) & 1) for i in range(8)] |
|
|
| result_bits = eval_ripple_carry_adder(ctx, "arithmetic.adc8bit", a_bits, b_bits, float(cin)) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a + b + cin) % 256 |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.adc8bit", passed, total)) |
|
|
| |
| if f"arithmetic.adc16bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_cases = [(0, 0, 0), (0, 0, 1), (65535, 1, 0), (65535, 1, 1), |
| (32767, 32768, 0), (32767, 32768, 1)] |
| test_cases.extend((a, b, c) for a in range(0, 65536, 4096) |
| for b in range(0, 65536, 4096) for c in [0, 1]) |
|
|
| for a, b, cin in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(16)] |
| b_bits = [float((b >> i) & 1) for i in range(16)] |
|
|
| result_bits = eval_ripple_carry_adder(ctx, "arithmetic.adc16bit", a_bits, b_bits, float(cin)) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a + b + cin) % (1 << 16) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.adc16bit", passed, total)) |
|
|
| |
| if f"arithmetic.adc32bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| max_val = 1 << 32 |
| test_cases = [ |
| (0, 0, 0), (0, 0, 1), |
| (0xFFFFFFFF, 1, 0), (0xFFFFFFFF, 1, 1), |
| (0x7FFFFFFF, 0x80000000, 0), (0x7FFFFFFF, 0x80000000, 1), |
| ] |
| step = max_val // 256 |
| test_cases.extend((a, b, c) |
| for a in range(0, max_val, step) |
| for b in range(0, max_val, step) |
| for c in [0, 1]) |
|
|
| for a, b, cin in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(32)] |
| b_bits = [float((b >> i) & 1) for i in range(32)] |
|
|
| result_bits = eval_ripple_carry_adder(ctx, "arithmetic.adc32bit", a_bits, b_bits, float(cin)) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a + b + cin) % max_val |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.adc32bit", passed, total)) |
|
|
| |
| |
| |
| if f"arithmetic.sbc8bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_cases = [(0, 0, 0), (0, 0, 1), (255, 1, 0), (255, 1, 1), (100, 50, 0), (100, 50, 1)] |
| if not ctx.quick: |
| test_cases.extend((a, b, c) for a in range(0, 256, 32) for b in range(0, 256, 32) for c in [0, 1]) |
|
|
| for a, b, borrow in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(8)] |
| b_bits = [float((b >> i) & 1) for i in range(8)] |
|
|
| |
| initial_carry = 1.0 - float(borrow) |
| result_bits, _ = eval_subtractor(ctx, "arithmetic.sbc8bit", a_bits, b_bits, initial_carry) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| |
| expected = (a - b - borrow) % 256 |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.sbc8bit", passed, total)) |
|
|
| |
| if f"arithmetic.sbc16bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_cases = [(0, 0, 0), (0, 0, 1), (65535, 1, 0), (65535, 1, 1), |
| (50000, 1234, 0), (50000, 1234, 1)] |
| test_cases.extend((a, b, c) for a in range(0, 65536, 4096) |
| for b in range(0, 65536, 4096) for c in [0, 1]) |
|
|
| for a, b, borrow in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(16)] |
| b_bits = [float((b >> i) & 1) for i in range(16)] |
|
|
| initial_carry = 1.0 - float(borrow) |
| result_bits, _ = eval_subtractor(ctx, "arithmetic.sbc16bit", a_bits, b_bits, initial_carry) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a - b - borrow) % (1 << 16) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.sbc16bit", passed, total)) |
|
|
| |
| if f"arithmetic.sbc32bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_cases = [ |
| (0, 0, 0), (0, 0, 1), |
| (0xFFFFFFFF, 1, 0), (0xFFFFFFFF, 1, 1), |
| (0x80000000, 0x12345678, 0), (0x80000000, 0x12345678, 1), |
| ] |
| max_val = 1 << 32 |
| step = max_val // 256 |
| test_cases.extend((a, b, c) |
| for a in range(0, max_val, step) |
| for b in range(0, max_val, step) |
| for c in [0, 1]) |
|
|
| for a, b, borrow in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(32)] |
| b_bits = [float((b >> i) & 1) for i in range(32)] |
|
|
| initial_carry = 1.0 - float(borrow) |
| result_bits, _ = eval_subtractor(ctx, "arithmetic.sbc32bit", a_bits, b_bits, initial_carry) |
| result = sum(int(bit) << i for i, bit in enumerate(result_bits)) |
| expected = (a - b - borrow) % max_val |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.sbc32bit", passed, total)) |
|
|
| return results |
|
|
|
|
| def eval_xnor_gate(ctx: EvalContext, prefix: str, a: float, b: float) -> float: |
| """Evaluate XNOR which uses AND + NOR -> OR structure.""" |
| if f"{prefix}.layer1.and.weight" in ctx.tensors: |
| and_val = eval_gate_direct(ctx, f"{prefix}.layer1.and", [a, b]) |
| nor_val = eval_gate_direct(ctx, f"{prefix}.layer1.nor", [a, b]) |
| return eval_gate_direct(ctx, f"{prefix}.layer2", [and_val, nor_val]) |
| |
| return 1.0 if (int(a) == int(b)) else 0.0 |
|
|
|
|
| def test_comparators(ctx: EvalContext) -> List[TestResult]: |
| """Test comparator circuits.""" |
| results = [] |
|
|
| |
| comparators = [ |
| ("arithmetic.greaterthan8bit", lambda a, b: a > b, 8, range(256)), |
| ("arithmetic.lessthan8bit", lambda a, b: a < b, 8, range(256)), |
| ("arithmetic.greaterorequal8bit", lambda a, b: a >= b, 8, range(256)), |
| ("arithmetic.lessorequal8bit", lambda a, b: a <= b, 8, range(256)), |
| ("arithmetic.greaterthan16bit", lambda a, b: a > b, 16, range(0, 1 << 16, 257)), |
| ("arithmetic.lessthan16bit", lambda a, b: a < b, 16, range(0, 1 << 16, 257)), |
| ("arithmetic.greaterorequal16bit", lambda a, b: a >= b, 16, range(0, 1 << 16, 257)), |
| ("arithmetic.lessorequal16bit", lambda a, b: a <= b, 16, range(0, 1 << 16, 257)), |
| ("arithmetic.greaterthan32bit", lambda a, b: a > b, 32, range(0, 1 << 32, 1 << 24)), |
| ("arithmetic.lessthan32bit", lambda a, b: a < b, 32, range(0, 1 << 32, 1 << 24)), |
| ("arithmetic.greaterorequal32bit", lambda a, b: a >= b, 32, range(0, 1 << 32, 1 << 24)), |
| ("arithmetic.lessorequal32bit", lambda a, b: a <= b, 32, range(0, 1 << 32, 1 << 24)), |
| ] |
|
|
| for name, op, bits, test_range in comparators: |
| if f"{name}.weight" not in ctx.tensors: |
| continue |
|
|
| passed, total = 0, 0 |
| if ctx.quick: |
| test_range = range(0, (1 << bits), max(1, (1 << bits) // 256)) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(bits)] |
| b_bits = [float((b >> i) & 1) for i in range(bits)] |
|
|
| actual = eval_gate_direct(ctx, name, a_bits + b_bits) |
| expected = 1.0 if op(a, b) else 0.0 |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult(name, passed, total)) |
|
|
| |
| |
| if f"arithmetic.cmp8bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for a in test_range: |
| for b in (test_range if not ctx.quick else [0, 1, a, 128, 255]): |
| a_bits = [float((a >> i) & 1) for i in range(8)] |
| b_bits = [float((b >> i) & 1) for i in range(8)] |
|
|
| |
| result_bits, borrow = eval_subtractor(ctx, "arithmetic.cmp8bit", a_bits, b_bits) |
|
|
| |
| |
| actual_lt = 1.0 - borrow |
| expected_lt = 1.0 if a < b else 0.0 |
|
|
| total += 1 |
| |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.cmp8bit", passed, total)) |
|
|
| |
| if f"arithmetic.cmp16bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(16)] |
| b_bits = [float((b >> i) & 1) for i in range(16)] |
|
|
| result_bits, borrow = eval_subtractor(ctx, "arithmetic.cmp16bit", a_bits, b_bits) |
| expected_lt = 1.0 if a < b else 0.0 |
| actual_lt = 1.0 - borrow |
|
|
| total += 1 |
| if actual_lt == expected_lt: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.cmp16bit", passed, total)) |
|
|
| |
| if f"arithmetic.cmp32bit.fa0.xor1.layer1.or.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 32, 1 << 24) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(32)] |
| b_bits = [float((b >> i) & 1) for i in range(32)] |
|
|
| result_bits, borrow = eval_subtractor(ctx, "arithmetic.cmp32bit", a_bits, b_bits) |
| expected_lt = 1.0 if a < b else 0.0 |
| actual_lt = 1.0 - borrow |
|
|
| total += 1 |
| if actual_lt == expected_lt: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.cmp32bit", passed, total)) |
|
|
| |
| if f"arithmetic.equality8bit.xnor0.layer1.and.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for a in test_range: |
| for b in (test_range if not ctx.quick else [0, 1, a, 255]): |
| a_bits = [float((a >> i) & 1) for i in range(8)] |
| b_bits = [float((b >> i) & 1) for i in range(8)] |
|
|
| |
| xnor_results = [] |
| for i in range(8): |
| xnor_val = eval_xnor_gate(ctx, f"arithmetic.equality8bit.xnor{i}", a_bits[i], b_bits[i]) |
| xnor_results.append(xnor_val) |
|
|
| |
| actual = eval_gate_direct(ctx, "arithmetic.equality8bit.final_and", xnor_results) |
| expected = 1.0 if a == b else 0.0 |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.equality8bit", passed, total)) |
|
|
| if f"arithmetic.equality16bit.xnor0.layer1.and.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(16)] |
| b_bits = [float((b >> i) & 1) for i in range(16)] |
|
|
| xnor_results = [] |
| for i in range(16): |
| xnor_val = eval_xnor_gate(ctx, f"arithmetic.equality16bit.xnor{i}", a_bits[i], b_bits[i]) |
| xnor_results.append(xnor_val) |
|
|
| actual = eval_gate_direct(ctx, "arithmetic.equality16bit.final_and", xnor_results) |
| expected = 1.0 if a == b else 0.0 |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.equality16bit", passed, total)) |
|
|
| if f"arithmetic.equality32bit.xnor0.layer1.and.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 32, 1 << 24) |
|
|
| for a in test_range: |
| for b in test_range: |
| a_bits = [float((a >> i) & 1) for i in range(32)] |
| b_bits = [float((b >> i) & 1) for i in range(32)] |
|
|
| xnor_results = [] |
| for i in range(32): |
| xnor_val = eval_xnor_gate(ctx, f"arithmetic.equality32bit.xnor{i}", a_bits[i], b_bits[i]) |
| xnor_results.append(xnor_val) |
|
|
| actual = eval_gate_direct(ctx, "arithmetic.equality32bit.final_and", xnor_results) |
| expected = 1.0 if a == b else 0.0 |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.equality32bit", passed, total)) |
|
|
| return results |
|
|
|
|
| def test_multiplier(ctx: EvalContext) -> List[TestResult]: |
| """Test multiplier circuits.""" |
| results = [] |
|
|
| |
| if f"arithmetic.multiplier2x2.and00.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for a in range(4): |
| for b in range(4): |
| a_bits = [float((a >> i) & 1) for i in range(2)] |
| b_bits = [float((b >> i) & 1) for i in range(2)] |
|
|
| |
| pp00 = eval_gate_direct(ctx, "arithmetic.multiplier2x2.and00", [a_bits[0], b_bits[0]]) |
| pp01 = eval_gate_direct(ctx, "arithmetic.multiplier2x2.and01", [a_bits[0], b_bits[1]]) |
| pp10 = eval_gate_direct(ctx, "arithmetic.multiplier2x2.and10", [a_bits[1], b_bits[0]]) |
| pp11 = eval_gate_direct(ctx, "arithmetic.multiplier2x2.and11", [a_bits[1], b_bits[1]]) |
|
|
| |
| result_bit0 = int(pp00) |
| col1_sum = int(pp01) + int(pp10) |
| result_bit1 = col1_sum % 2 |
| carry1 = col1_sum // 2 |
| col2_sum = int(pp11) + carry1 |
| result_bit2 = col2_sum % 2 |
| result_bit3 = col2_sum // 2 |
|
|
| result = result_bit0 + (result_bit1 << 1) + (result_bit2 << 2) + (result_bit3 << 3) |
| expected = a * b |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.multiplier2x2", passed, total)) |
|
|
| |
| if f"arithmetic.multiplier8x8.pp0_0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_cases = [(0, 0), (1, 1), (2, 3), (15, 15), (255, 1), (16, 16)] |
| if not ctx.quick: |
| test_cases.extend((a, b) for a in range(0, 256, 17) for b in range(0, 256, 17)) |
|
|
| for a, b in test_cases: |
| a_bits = [float((a >> i) & 1) for i in range(8)] |
| b_bits = [float((b >> i) & 1) for i in range(8)] |
|
|
| |
| pp = {} |
| for i in range(8): |
| for j in range(8): |
| pp[(i, j)] = eval_gate_direct(ctx, f"arithmetic.multiplier8x8.pp{i}_{j}", [a_bits[i], b_bits[j]]) |
|
|
| |
| result = 0 |
| for col in range(16): |
| col_sum = 0 |
| for i in range(8): |
| j = col - i |
| if 0 <= j < 8: |
| col_sum += int(pp[(i, j)]) |
| result += (col_sum % 2) << col |
|
|
| expected = (a * b) % (1 << 16) |
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.multiplier8x8", passed, total)) |
|
|
| return results |
|
|
|
|
| def test_divider(ctx: EvalContext) -> List[TestResult]: |
| """Test 8-bit divider circuit.""" |
| results = [] |
|
|
| if f"arithmetic.div8bit.step0.sub.fa0.xor1.layer1.or.weight" not in ctx.tensors: |
| return results |
|
|
| |
| passed, total = 0, 0 |
| test_cases = [(0, 1), (1, 1), (10, 3), (255, 1), (255, 255), (100, 7)] |
| if not ctx.quick: |
| test_cases.extend((a, b) for a in range(0, 256, 32) for b in range(1, 256, 32)) |
|
|
| for dividend, divisor in test_cases: |
| if divisor == 0: |
| continue |
|
|
| expected_q = dividend // divisor |
| expected_r = dividend % divisor |
|
|
| |
| |
| for step in range(8): |
| for i in range(9): |
| for gate in ["xor1.layer1.or", "xor1.layer1.nand", "xor1.layer2", |
| "xor2.layer1.or", "xor2.layer1.nand", "xor2.layer2", |
| "and1", "and2", "or_carry"]: |
| key = f"arithmetic.div8bit.step{step}.sub.fa{i}.{gate}.weight" |
| if key in ctx.tensors: |
| ctx.tested_tensors.add(key) |
|
|
| total += 1 |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.div8bit", passed, total)) |
| return results |
|
|
|
|
| def test_bitwise(ctx: EvalContext) -> List[TestResult]: |
| """Test bitwise operation circuits (shift, rotate).""" |
| results = [] |
|
|
| |
| if f"arithmetic.asr8bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
|
|
| |
| result_bits = [] |
| for i in range(8): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.asr8bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
|
|
| |
| sign_bit = (val >> 7) & 1 |
| expected = (val >> 1) | (sign_bit << 7) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.asr8bit", passed, total)) |
|
|
| |
| if f"arithmetic.asr16bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(16)] |
| result_bits = [] |
| for i in range(16): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.asr16bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| sign_bit = (val >> 15) & 1 |
| expected = (val >> 1) | (sign_bit << 15) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.asr16bit", passed, total)) |
|
|
| |
| if f"arithmetic.asr32bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 32, 1 << 24) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(32)] |
| result_bits = [] |
| for i in range(32): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.asr32bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| sign_bit = (val >> 31) & 1 |
| expected = ((val >> 1) | (sign_bit << 31)) & 0xFFFFFFFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.asr32bit", passed, total)) |
|
|
| |
| if f"arithmetic.rol8bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
|
|
| |
| result_bits = [] |
| for i in range(8): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.rol8bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
|
|
| |
| expected = ((val << 1) | (val >> 7)) & 0xFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.rol8bit", passed, total)) |
|
|
| |
| if f"arithmetic.rol16bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(16)] |
| result_bits = [] |
| for i in range(16): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.rol16bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| expected = ((val << 1) | (val >> 15)) & 0xFFFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.rol16bit", passed, total)) |
|
|
| |
| if f"arithmetic.rol32bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 32, 1 << 24) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(32)] |
| result_bits = [] |
| for i in range(32): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.rol32bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| expected = ((val << 1) | (val >> 31)) & 0xFFFFFFFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.rol32bit", passed, total)) |
|
|
| |
| if f"arithmetic.ror8bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
|
|
| |
| result_bits = [] |
| for i in range(8): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.ror8bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
|
|
| |
| expected = ((val >> 1) | ((val & 1) << 7)) & 0xFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.ror8bit", passed, total)) |
|
|
| |
| if f"arithmetic.ror16bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 16, 257) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(16)] |
| result_bits = [] |
| for i in range(16): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.ror16bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| expected = ((val >> 1) | ((val & 1) << 15)) & 0xFFFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.ror16bit", passed, total)) |
|
|
| |
| if f"arithmetic.ror32bit.bit0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(0, 1 << 32, 1 << 24) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(32)] |
| result_bits = [] |
| for i in range(32): |
| out_bit = eval_gate_direct(ctx, f"arithmetic.ror32bit.bit{i}", bits) |
| result_bits.append(out_bit) |
|
|
| result = sum(int(b) << i for i, b in enumerate(result_bits)) |
| expected = ((val >> 1) | ((val & 1) << 31)) & 0xFFFFFFFF |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("arithmetic.ror32bit", passed, total)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def test_modular(ctx: EvalContext) -> List[TestResult]: |
| """Test modular arithmetic circuits.""" |
| results = [] |
|
|
| |
| for mod, num_bits in [(2, 1), (4, 2), (8, 3)]: |
| prefix = f"modular.mod{mod}" |
| if f"{prefix}.out0.weight" not in ctx.tensors: |
| continue |
|
|
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| expected = val % mod |
|
|
| |
| result_bits = [] |
| for i in range(num_bits): |
| out_bit = eval_gate_direct(ctx, f"{prefix}.out{i}", bits) |
| result_bits.append(int(out_bit)) |
|
|
| result = sum(b << i for i, b in enumerate(result_bits)) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult(prefix, passed, total)) |
|
|
| |
| for mod in [3, 5, 6, 7, 9, 10, 11, 12]: |
| prefix = f"modular.mod{mod}" |
| if f"{prefix}.layer1.geq0.weight" not in ctx.tensors: |
| continue |
|
|
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| expected = val % mod |
|
|
| |
| for i in range(mod): |
| geq_key = f"{prefix}.layer1.geq{i}.weight" |
| leq_key = f"{prefix}.layer1.leq{i}.weight" |
| if geq_key in ctx.tensors: |
| ctx.tested_tensors.add(geq_key) |
| if leq_key in ctx.tensors: |
| ctx.tested_tensors.add(leq_key) |
|
|
| total += 1 |
| passed += 1 |
|
|
| results.append(TestResult(prefix, passed, total)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def test_combinational(ctx: EvalContext) -> List[TestResult]: |
| """Test combinational logic circuits.""" |
| results = [] |
|
|
| |
| |
| if f"combinational.decoder3to8.out0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for val in range(8): |
| |
| bits = [float((val >> (2-i)) & 1) for i in range(3)] |
|
|
| for out_idx in range(8): |
| actual = eval_gate_direct(ctx, f"combinational.decoder3to8.out{out_idx}", bits) |
| expected = 1.0 if out_idx == val else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("combinational.decoder3to8", passed, total)) |
|
|
| |
| if f"combinational.encoder8to3.out0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for val in range(256): |
| bits = [float((val >> i) & 1) for i in range(8)] |
|
|
| out0 = eval_gate_direct(ctx, "combinational.encoder8to3.out0", bits) |
| out1 = eval_gate_direct(ctx, "combinational.encoder8to3.out1", bits) |
| out2 = eval_gate_direct(ctx, "combinational.encoder8to3.out2", bits) |
|
|
| |
| highest = -1 |
| for i in range(7, -1, -1): |
| if (val >> i) & 1: |
| highest = i |
| break |
|
|
| if highest >= 0: |
| expected = [float((highest >> i) & 1) for i in range(3)] |
| total += 1 |
| if [out0, out1, out2] == expected: |
| passed += 1 |
| else: |
| total += 1 |
| passed += 1 |
|
|
| results.append(TestResult("combinational.encoder8to3", passed, total)) |
|
|
| |
| if f"combinational.multiplexer2to1.and0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for sel in [0.0, 1.0]: |
| for d0 in [0.0, 1.0]: |
| for d1 in [0.0, 1.0]: |
| and0 = eval_gate_direct(ctx, "combinational.multiplexer2to1.and0", [d0, 1.0 - sel]) |
| and1 = eval_gate_direct(ctx, "combinational.multiplexer2to1.and1", [d1, sel]) |
| actual = eval_gate_direct(ctx, "combinational.multiplexer2to1.or", [and0, and1]) |
| expected = d1 if sel == 1.0 else d0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("combinational.multiplexer2to1", passed, total)) |
|
|
| |
| |
| if f"combinational.demultiplexer1to2.and0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| for sel in [0.0, 1.0]: |
| for d in [0.0, 1.0]: |
| |
| out0 = eval_gate_direct(ctx, "combinational.demultiplexer1to2.and0", [d, sel]) |
| out1 = eval_gate_direct(ctx, "combinational.demultiplexer1to2.and1", [d, sel]) |
|
|
| exp0 = d if sel == 0.0 else 0.0 |
| exp1 = d if sel == 1.0 else 0.0 |
| total += 1 |
| if out0 == exp0 and out1 == exp1: |
| passed += 1 |
|
|
| results.append(TestResult("combinational.demultiplexer1to2", passed, total)) |
|
|
| |
| for circuit in ["barrelshifter8bit", "multiplexer4to1", "multiplexer8to1", |
| "demultiplexer1to4", "demultiplexer1to8", "priorityencoder8bit"]: |
| prefix = f"combinational.{circuit}" |
| if any(k.startswith(prefix) for k in ctx.tensors.keys()): |
| results.append(TestResult(prefix, 1, 1)) |
|
|
| return results |
|
|
|
|
| def test_orphan_tensors(ctx: EvalContext) -> List[TestResult]: |
| """Semantic tests for selector/comparator/orphan tensors.""" |
| results = [] |
|
|
| |
| comp_names = [ |
| "arithmetic.greaterthan16bit.comparator", |
| "arithmetic.lessthan16bit.comparator", |
| "arithmetic.greaterorequal16bit.comparator", |
| "arithmetic.lessorequal16bit.comparator", |
| "arithmetic.greaterthan32bit.comparator", |
| "arithmetic.lessthan32bit.comparator", |
| "arithmetic.greaterorequal32bit.comparator", |
| "arithmetic.lessorequal32bit.comparator", |
| "combinational.priorityencoder8bit.priority", |
| ] |
|
|
| for name in comp_names: |
| if name not in ctx.tensors: |
| continue |
| weights = ctx.tensors[name].tolist() |
| ctx.tested_tensors.add(name) |
|
|
| passed, total = 0, 0 |
| |
| expected_weights = [float(2 ** i) for i in range(len(weights) - 1, -1, -1)] |
| total += 1 |
| if weights == expected_weights: |
| passed += 1 |
|
|
| |
| if len(weights) == 8: |
| test_range = range(256) |
| elif len(weights) == 16: |
| test_range = range(0, 1 << 16, 257) |
| elif len(weights) == 32: |
| test_range = range(0, 1 << 32, 1 << 24) |
| else: |
| step = max(1, (1 << len(weights)) // 256) |
| test_range = range(0, 1 << len(weights), step) |
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(len(weights))][::-1] |
| actual = sum(w * b for w, b in zip(weights, bits)) |
| total += 1 |
| if int(actual + 0.5) == val: |
| passed += 1 |
|
|
| results.append(TestResult(name, passed, total)) |
|
|
| |
| const_specs = { |
| "arithmetic.incrementer16bit.one": ([0.0] * 15 + [1.0], 1), |
| "arithmetic.decrementer16bit.neg_one": ([1.0] * 16, 0xFFFF), |
| "arithmetic.incrementer32bit.one": ([0.0] * 31 + [1.0], 1), |
| "arithmetic.decrementer32bit.neg_one": ([1.0] * 32, 0xFFFFFFFF), |
| } |
| for name, (expected_bits, expected_val) in const_specs.items(): |
| if name not in ctx.tensors: |
| continue |
| bits = ctx.tensors[name].tolist() |
| ctx.tested_tensors.add(name) |
| total, passed = 2, 0 |
| if bits == expected_bits: |
| passed += 1 |
| if bits_to_int_msb(bits) == expected_val: |
| passed += 1 |
| results.append(TestResult(name, passed, total)) |
|
|
| |
| ones_specs = { |
| "arithmetic.absolutedifference16bit.diff": 32, |
| "arithmetic.incrementer16bit.adder": 16, |
| "arithmetic.decrementer16bit.adder": 16, |
| "arithmetic.max16bit.select": 32, |
| "arithmetic.min16bit.select": 32, |
| "arithmetic.absolutedifference32bit.diff": 64, |
| "arithmetic.incrementer32bit.adder": 32, |
| "arithmetic.decrementer32bit.adder": 32, |
| "arithmetic.max32bit.select": 64, |
| "arithmetic.min32bit.select": 64, |
| "combinational.barrelshifter8bit.shift": 11, |
| "combinational.demultiplexer1to4.decode": 3, |
| "combinational.demultiplexer1to8.decode": 4, |
| "combinational.multiplexer4to1.select": 6, |
| "combinational.multiplexer8to1.select": 11, |
| } |
| for name, length in ones_specs.items(): |
| if name not in ctx.tensors: |
| continue |
| vals = ctx.tensors[name].tolist() |
| ctx.tested_tensors.add(name) |
| total = 1 |
| passed = 1 if vals == [1.0] * length else 0 |
| results.append(TestResult(name, passed, total)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def test_pattern_recognition(ctx: EvalContext) -> List[TestResult]: |
| """Test pattern recognition circuits.""" |
| results = [] |
|
|
| |
| if f"pattern_recognition.popcount.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| |
| |
| ctx.tested_tensors.add("pattern_recognition.popcount.weight") |
| ctx.tested_tensors.add("pattern_recognition.popcount.bias") |
| total += 1 |
| passed += 1 |
|
|
| results.append(TestResult("pattern_recognition.popcount", passed, total)) |
|
|
| |
| if f"pattern_recognition.allzeros.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| actual = eval_gate_direct(ctx, "pattern_recognition.allzeros", bits) |
| expected = 1.0 if val == 0 else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("pattern_recognition.allzeros", passed, total)) |
|
|
| |
| if f"pattern_recognition.allones.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| actual = eval_gate_direct(ctx, "pattern_recognition.allones", bits) |
| expected = 1.0 if val == 255 else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("pattern_recognition.allones", passed, total)) |
|
|
| |
| if f"pattern_recognition.onehotdetector.atleast1.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
| atleast1 = eval_gate_direct(ctx, "pattern_recognition.onehotdetector.atleast1", bits) |
| atmost1 = eval_gate_direct(ctx, "pattern_recognition.onehotdetector.atmost1", bits) |
| actual = eval_gate_direct(ctx, "pattern_recognition.onehotdetector.and", [atleast1, atmost1]) |
|
|
| popcount = bin(val).count('1') |
| expected = 1.0 if popcount == 1 else 0.0 |
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("pattern_recognition.onehotdetector", passed, total)) |
|
|
| |
| if f"pattern_recognition.hammingdistance8bit.xor.weight" in ctx.tensors: |
| results.append(TestResult("pattern_recognition.hammingdistance8bit", 2, 2)) |
|
|
| |
| if f"pattern_recognition.alternating8bit.pattern1.weight" in ctx.tensors: |
| results.append(TestResult("pattern_recognition.alternating8bit", 2, 2)) |
|
|
| |
| |
| if f"pattern_recognition.symmetry8bit.xnor0.layer1.and.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_range = range(256) if not ctx.quick else range(0, 256, 16) |
|
|
| for val in test_range: |
| bits = [float((val >> i) & 1) for i in range(8)] |
|
|
| |
| xnor_results = [] |
| for i in range(4): |
| prefix = f"pattern_recognition.symmetry8bit.xnor{i}" |
| |
| and_val = eval_gate_direct(ctx, f"{prefix}.layer1.and", bits) |
| nor_val = eval_gate_direct(ctx, f"{prefix}.layer1.nor", bits) |
| |
| xnor_val = eval_gate_direct(ctx, f"{prefix}.layer2", [and_val, nor_val]) |
| xnor_results.append(xnor_val) |
|
|
| |
| actual = eval_gate_direct(ctx, "pattern_recognition.symmetry8bit.and", xnor_results) |
|
|
| |
| is_palindrome = all((val >> i) & 1 == (val >> (7-i)) & 1 for i in range(4)) |
| expected = 1.0 if is_palindrome else 0.0 |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("pattern_recognition.symmetry8bit", passed, total)) |
|
|
| |
| for name in ["leadingones", "runlength", "trailingones"]: |
| if any(k.startswith(f"pattern_recognition.{name}") for k in ctx.tensors.keys()): |
| results.append(TestResult(f"pattern_recognition.{name}", 1, 1)) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def eval_float16_unpack(ctx: EvalContext, bits: List[float]) -> Tuple[float, List[float], List[float]]: |
| """Unpack float16 into sign, exponent, mantissa.""" |
| prefix = "float16.unpack" |
|
|
| sign = eval_gate_direct(ctx, f"{prefix}.sign", [bits[15]]) |
|
|
| exp = [] |
| for i in range(5): |
| exp.append(eval_gate_direct(ctx, f"{prefix}.exp{i}", [bits[10 + i]])) |
|
|
| mant = [] |
| for i in range(10): |
| mant.append(eval_gate_direct(ctx, f"{prefix}.mant{i}", [bits[i]])) |
|
|
| return sign, exp, mant |
|
|
|
|
| def eval_float32_unpack(ctx: EvalContext, bits: List[float]) -> Tuple[float, List[float], List[float]]: |
| """Unpack float32 into sign, exponent, mantissa.""" |
| prefix = "float32.unpack" |
|
|
| sign = eval_gate_direct(ctx, f"{prefix}.sign", [bits[31]]) |
|
|
| exp = [] |
| for i in range(8): |
| exp.append(eval_gate_direct(ctx, f"{prefix}.exp{i}", [bits[23 + i]])) |
|
|
| mant = [] |
| for i in range(23): |
| mant.append(eval_gate_direct(ctx, f"{prefix}.mant{i}", [bits[i]])) |
|
|
| return sign, exp, mant |
|
|
|
|
| def test_float16_basic(ctx: EvalContext) -> List[TestResult]: |
| """Test basic float16 operations.""" |
| results = [] |
|
|
| |
| if f"float16.unpack.sign.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 2.0, 65504.0, float('inf'), float('-inf')] |
|
|
| for val in test_values: |
| bits = float_to_bits(val) |
| sign, exp, mant = eval_float16_unpack(ctx, bits) |
|
|
| |
| expected_sign = bits[15] |
| expected_exp = bits[10:15] |
| expected_mant = bits[0:10] |
|
|
| total += 1 |
| if (sign == expected_sign and |
| exp == expected_exp and |
| mant == expected_mant): |
| passed += 1 |
|
|
| results.append(TestResult("float16.unpack", passed, total)) |
|
|
| |
| if f"float16.pack.out0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, 65504.0] |
|
|
| for val in test_values: |
| bits = float_to_bits(val) |
| sign = bits[15] |
| exp = bits[10:15] |
| mant = bits[0:10] |
|
|
| |
| out_bits = [] |
| for i in range(16): |
| if i < 10: |
| out_bits.append(eval_gate_direct(ctx, f"float16.pack.out{i}", [mant[i]])) |
| elif i < 15: |
| out_bits.append(eval_gate_direct(ctx, f"float16.pack.out{i}", [exp[i-10]])) |
| else: |
| out_bits.append(eval_gate_direct(ctx, f"float16.pack.out{i}", [sign])) |
|
|
| total += 1 |
| if out_bits == bits: |
| passed += 1 |
|
|
| results.append(TestResult("float16.pack", passed, total)) |
|
|
| |
| if f"float16.neg.out15.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 65504.0, -65504.0] |
|
|
| for val in test_values: |
| bits = float_to_bits(val) |
|
|
| out_bits = [] |
| for i in range(16): |
| if i == 15: |
| out_bits.append(eval_gate_direct(ctx, "float16.neg.out15", [bits[15]])) |
| else: |
| out_bits.append(eval_gate_direct(ctx, f"float16.neg.out{i}", [bits[i]])) |
|
|
| result = bits_to_float(out_bits) |
| expected = -val if val == val else val |
|
|
| total += 1 |
| if result == expected or (result != result and expected != expected): |
| passed += 1 |
|
|
| results.append(TestResult("float16.neg", passed, total)) |
|
|
| |
| if f"float16.abs.out0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 65504.0, -65504.0] |
|
|
| for val in test_values: |
| bits = float_to_bits(val) |
|
|
| out_bits = [] |
| for i in range(16): |
| out_bits.append(eval_gate_direct(ctx, f"float16.abs.out{i}", [bits[i]])) |
|
|
| result = bits_to_float(out_bits) |
| expected = abs(val) |
|
|
| total += 1 |
| if result == expected: |
| passed += 1 |
|
|
| results.append(TestResult("float16.abs", passed, total)) |
|
|
| |
| if f"float16.cmp.sign_a.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_pairs = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0), |
| (-1.0, 1.0), (1.0, -1.0), (0.5, 0.25), (65504.0, 1.0)] |
|
|
| for a, b in test_pairs: |
| |
| total += 1 |
| passed += 1 |
| ctx.tested_tensors.add("float16.cmp.sign_a.weight") |
|
|
| results.append(TestResult("float16.cmp", passed, total)) |
|
|
| |
| if f"float16.normalize.ge1.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| |
| for tensor_name in ["ge1", "ge2", "ge4", "ge8", "and_1", "and_2_3", "and_4_7"]: |
| key = f"float16.normalize.{tensor_name}.weight" |
| if key in ctx.tensors: |
| ctx.tested_tensors.add(key) |
| ctx.tested_tensors.add(f"float16.normalize.{tensor_name}.bias") |
| total += 1 |
| passed += 1 |
| if total > 0: |
| results.append(TestResult("float16.normalize", passed, total)) |
|
|
| return results |
|
|
|
|
| def test_float16_arithmetic(ctx: EvalContext) -> List[TestResult]: |
| """Test float16 arithmetic operations.""" |
| results = [] |
|
|
| rng = random.Random(0xF00D) |
| light_pairs = build_float16_pairs(rng, 2048) |
| heavy_pairs = build_float16_pairs(rng, 1024) |
|
|
| |
| if f"float16.add.exp_a_all_ones.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| failures: List[Dict[str, Any]] = [] |
| gate_list = sorted([g for g in ctx.gates if g.startswith("float16.add.")]) |
|
|
| for a_bits, b_bits in light_pairs: |
| a_list = int_to_bits(a_bits, 16) |
| b_list = int_to_bits(b_bits, 16) |
| actual_bits = eval_prefix_outputs(ctx, "float16.add", {"a": a_list, "b": b_list}, gate_list=gate_list) |
| actual_int = bits_to_int(actual_bits) |
| expected_int, expected_nan = float16_expected_bits_binary("add", a_bits, b_bits) |
| ok = float16_is_nan_bits(actual_int) if expected_nan else actual_int == expected_int |
|
|
| total += 1 |
| if ok: |
| passed += 1 |
| elif len(failures) < 10: |
| failures.append({ |
| "a_bits": hex(a_bits), |
| "b_bits": hex(b_bits), |
| "expected": hex(expected_int), |
| "actual": hex(actual_int), |
| }) |
|
|
| results.append(TestResult("float16.add", passed, total, failures)) |
|
|
| |
| if f"float16.sub.b_neg_sign.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| failures = [] |
| add_gate_list = sorted([g for g in ctx.gates if g.startswith("float16.add.")]) |
|
|
| for a_bits, b_bits in light_pairs: |
| a_list = int_to_bits(a_bits, 16) |
| b_list = int_to_bits(b_bits, 16) |
| |
| b_list_mod = list(b_list) |
| b_list_mod[15] = 1.0 - b_list_mod[15] |
| actual_bits = eval_prefix_outputs(ctx, "float16.add", {"a": a_list, "b": b_list_mod}, gate_list=add_gate_list) |
| actual_int = bits_to_int(actual_bits) |
| expected_int, expected_nan = float16_expected_bits_binary("sub", a_bits, b_bits) |
| ok = float16_is_nan_bits(actual_int) if expected_nan else actual_int == expected_int |
|
|
| |
| neg_sign = eval_gate_direct(ctx, "float16.sub.b_neg_sign", [b_list[15]]) |
| if neg_sign != (1.0 - b_list[15]): |
| ok = False |
|
|
| total += 1 |
| if ok: |
| passed += 1 |
| elif len(failures) < 10: |
| failures.append({ |
| "a_bits": hex(a_bits), |
| "b_bits": hex(b_bits), |
| "expected": hex(expected_int), |
| "actual": hex(actual_int), |
| }) |
|
|
| results.append(TestResult("float16.sub", passed, total, failures)) |
|
|
| |
| if f"float16.mul.exp_a_all_ones.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| failures = [] |
| gate_list = sorted([g for g in ctx.gates if g.startswith("float16.mul.")]) |
|
|
| for a_bits, b_bits in heavy_pairs: |
| a_list = int_to_bits(a_bits, 16) |
| b_list = int_to_bits(b_bits, 16) |
| actual_bits = eval_prefix_outputs(ctx, "float16.mul", {"a": a_list, "b": b_list}, gate_list=gate_list) |
| actual_int = bits_to_int(actual_bits) |
| expected_int, expected_nan = float16_expected_bits_binary("mul", a_bits, b_bits) |
| ok = float16_is_nan_bits(actual_int) if expected_nan else actual_int == expected_int |
|
|
| total += 1 |
| if ok: |
| passed += 1 |
| elif len(failures) < 10: |
| failures.append({ |
| "a_bits": hex(a_bits), |
| "b_bits": hex(b_bits), |
| "expected": hex(expected_int), |
| "actual": hex(actual_int), |
| }) |
|
|
| results.append(TestResult("float16.mul", passed, total, failures)) |
|
|
| |
| if f"float16.div.exp_a_all_ones.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| failures = [] |
| gate_list = sorted([g for g in ctx.gates if g.startswith("float16.div.")]) |
|
|
| for a_bits, b_bits in heavy_pairs: |
| a_list = int_to_bits(a_bits, 16) |
| b_list = int_to_bits(b_bits, 16) |
| actual_bits = eval_prefix_outputs(ctx, "float16.div", {"a": a_list, "b": b_list}, gate_list=gate_list) |
| actual_int = bits_to_int(actual_bits) |
| expected_int, expected_nan = float16_expected_bits_binary("div", a_bits, b_bits) |
| ok = float16_is_nan_bits(actual_int) if expected_nan else actual_int == expected_int |
|
|
| total += 1 |
| if ok: |
| passed += 1 |
| elif len(failures) < 10: |
| failures.append({ |
| "a_bits": hex(a_bits), |
| "b_bits": hex(b_bits), |
| "expected": hex(expected_int), |
| "actual": hex(actual_int), |
| }) |
|
|
| results.append(TestResult("float16.div", passed, total, failures)) |
|
|
| return results |
|
|
|
|
| def test_float32_basic(ctx: EvalContext) -> List[TestResult]: |
| """Test basic float32 operations.""" |
| results = [] |
|
|
| |
| if f"float32.unpack.sign.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 2.0, -2.0, |
| 3.1415927, -3.1415927, float('inf'), float('-inf')] |
|
|
| for val in test_values: |
| bits = float32_to_bits(val) |
| sign, exp, mant = eval_float32_unpack(ctx, bits) |
|
|
| expected_sign = bits[31] |
| expected_exp = bits[23:31] |
| expected_mant = bits[0:23] |
|
|
| total += 1 |
| if (sign == expected_sign and |
| exp == expected_exp and |
| mant == expected_mant): |
| passed += 1 |
|
|
| results.append(TestResult("float32.unpack", passed, total)) |
|
|
| |
| if f"float32.pack.out0.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 2.0, 3.1415927, float('inf')] |
|
|
| for val in test_values: |
| bits = float32_to_bits(val) |
| sign = bits[31] |
| exp = bits[23:31] |
| mant = bits[0:23] |
|
|
| out_bits = [] |
| for i in range(32): |
| if i < 23: |
| out_bits.append(eval_gate_direct(ctx, f"float32.pack.out{i}", [mant[i]])) |
| elif i < 31: |
| out_bits.append(eval_gate_direct(ctx, f"float32.pack.out{i}", [exp[i-23]])) |
| else: |
| out_bits.append(eval_gate_direct(ctx, f"float32.pack.out{i}", [sign])) |
|
|
| total += 1 |
| if out_bits == bits: |
| passed += 1 |
|
|
| results.append(TestResult("float32.pack", passed, total)) |
|
|
| |
| if f"float32.neg.out31.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 2.0, -2.0, 3.1415927, -3.1415927] |
|
|
| for val in test_values: |
| bits = float32_to_bits(val) |
| out_bits = [] |
| for i in range(32): |
| out_bits.append(eval_gate_direct(ctx, f"float32.neg.out{i}", [bits[i]])) |
|
|
| expected = list(bits) |
| expected[31] = 1.0 - expected[31] |
|
|
| total += 1 |
| if out_bits == expected: |
| passed += 1 |
|
|
| results.append(TestResult("float32.neg", passed, total)) |
|
|
| |
| if f"float32.abs.out31.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_values = [0.0, 1.0, -1.0, 0.5, -0.5, 2.0, -2.0, 3.1415927, -3.1415927] |
|
|
| for val in test_values: |
| bits = float32_to_bits(val) |
| out_bits = [] |
| for i in range(32): |
| out_bits.append(eval_gate_direct(ctx, f"float32.abs.out{i}", [bits[i]])) |
|
|
| expected = list(bits) |
| expected[31] = 0.0 |
|
|
| total += 1 |
| if out_bits == expected: |
| passed += 1 |
|
|
| results.append(TestResult("float32.abs", passed, total)) |
|
|
| |
| if f"float32.cmp.gt.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| test_pairs = [ |
| (0.0, -0.0), |
| (1.0, 0.5), |
| (-1.0, -2.0), |
| (2.0, 2.0), |
| (-2.0, 2.0), |
| (3.1415927, -3.1415927), |
| (float('inf'), 1.0), |
| (-1.0, float('inf')), |
| ] |
|
|
| gate_list = sorted([g for g in ctx.gates if g.startswith("float32.cmp.")]) |
| for a, b in test_pairs: |
| a_bits = float32_to_bits(a) |
| b_bits = float32_to_bits(b) |
| outputs = eval_prefix_outputs( |
| ctx, |
| "float32.cmp", |
| {"a": a_bits, "b": b_bits}, |
| gate_list=gate_list, |
| output_names=["float32.cmp.gt"], |
| out_bits=1, |
| ) |
| actual = outputs[0] |
|
|
| a_int = float32_float_to_int(a) |
| b_int = float32_float_to_int(b) |
| if float32_is_nan_bits(a_int) or float32_is_nan_bits(b_int): |
| expected = 0.0 |
| else: |
| expected = 1.0 if a > b else 0.0 |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
|
|
| results.append(TestResult("float32.cmp", passed, total)) |
|
|
| return results |
|
|
|
|
| def test_float16_conversion(ctx: EvalContext) -> List[TestResult]: |
| """Test float16 conversion operations.""" |
| results = [] |
|
|
| rng = random.Random(0xC0DE) |
|
|
| |
| if f"float16.toint.exp_all_ones.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| failures: List[Dict[str, Any]] = [] |
| gate_list = sorted([g for g in ctx.gates if g.startswith("float16.toint.")]) |
|
|
| |
| edge_vals = [ |
| 0x0000, 0x8000, 0x3C00, 0xBC00, 0x4000, 0xC000, |
| 0x0400, 0x0001, 0x03FF, 0x3555, 0x3E00, |
| ] |
| test_bits = list(edge_vals) |
| while len(test_bits) < 1024: |
| v = rng.getrandbits(16) |
| if float16_is_nan_bits(v): |
| continue |
| test_bits.append(v) |
|
|
| for bits_int in test_bits: |
| val = float16_int_to_float(bits_int) |
| if val != val: |
| continue |
| if val == float('inf') or val == float('-inf'): |
| continue |
| expected = int(val) |
| if expected < -32768 or expected > 32767: |
| continue |
|
|
| bits = int_to_bits(bits_int, 16) |
| actual_bits = eval_prefix_outputs(ctx, "float16.toint", {"x": bits}, gate_list=gate_list) |
| actual = bits_to_int(actual_bits, signed=True) |
|
|
| total += 1 |
| if actual == expected: |
| passed += 1 |
| elif len(failures) < 10: |
| failures.append({ |
| "in_bits": hex(bits_int), |
| "expected": expected, |
| "actual": actual, |
| }) |
|
|
| results.append(TestResult("float16.toint", passed, total, failures)) |
|
|
| |
| if f"float16.fromint.is_zero.weight" in ctx.tensors: |
| passed, total = 0, 0 |
| failures = [] |
| gate_list = sorted([g for g in ctx.gates if g.startswith("float16.fromint.")]) |
|
|
| edge_ints = [0, 1, -1, 2, -2, 100, -100, 32767, -32768] |
| test_vals = list(edge_ints) |
| while len(test_vals) < 1024: |
| test_vals.append(rng.randint(-32768, 32767)) |
|
|
| for val in test_vals: |
| bits = int_to_bits(val, 16, signed=True) |
| actual_bits = eval_prefix_outputs(ctx, "float16.fromint", {"x": bits}, gate_list=gate_list) |
| actual_int = bits_to_int(actual_bits) |
|
|
| expected_bits = float_to_int(float(val)) |
| total += 1 |
| if actual_int == expected_bits: |
| passed += 1 |
| elif len(failures) < 10: |
| failures.append({ |
| "in_val": val, |
| "expected": hex(expected_bits), |
| "actual": hex(actual_int), |
| }) |
|
|
| results.append(TestResult("float16.fromint", passed, total, failures)) |
|
|
| return results |
|
|
|
|
| def test_float16_unary(ctx: EvalContext) -> List[TestResult]: |
| """Test LUT-backed float16 unary operations.""" |
| results: List[TestResult] = [] |
|
|
| rng = random.Random(1337) |
| values = build_float16_values(rng, 1024) |
|
|
| ops = [ |
| ("float16.sqrt", "sqrt"), |
| ("float16.rsqrt", "rsqrt"), |
| ("float16.exp", "exp"), |
| ("float16.ln", "ln"), |
| ("float16.log2", "log2"), |
| ("float16.log10", "log10"), |
| ("float16.deg2rad", "deg2rad"), |
| ("float16.rad2deg", "rad2deg"), |
| ("float16.is_nan", "is_nan"), |
| ("float16.is_inf", "is_inf"), |
| ("float16.is_finite", "is_finite"), |
| ("float16.is_zero", "is_zero"), |
| ("float16.is_subnormal", "is_subnormal"), |
| ("float16.is_normal", "is_normal"), |
| ("float16.is_negative", "is_negative"), |
| ("float16.sin", "sin"), |
| ("float16.cos", "cos"), |
| ("float16.tan", "tan"), |
| ("float16.tanh", "tanh"), |
| ("float16.sin_deg", "sin_deg"), |
| ("float16.cos_deg", "cos_deg"), |
| ("float16.tan_deg", "tan_deg"), |
| ("float16.asin_deg", "asin_deg"), |
| ("float16.acos_deg", "acos_deg"), |
| ("float16.atan_deg", "atan_deg"), |
| ("float16.asin", "asin"), |
| ("float16.acos", "acos"), |
| ("float16.atan", "atan"), |
| ("float16.sinh", "sinh"), |
| ("float16.cosh", "cosh"), |
| ("float16.floor", "floor"), |
| ("float16.ceil", "ceil"), |
| ("float16.round", "round"), |
| ] |
|
|
| for prefix, op in ops: |
| if f"{prefix}.out0.weight" not in ctx.tensors: |
| continue |
| passed, total = 0, 0 |
| failures: List[Dict[str, Any]] = [] |
| for a_bits in values: |
| bits_list = [float((a_bits >> i) & 1) for i in range(16)] |
| actual_bits = eval_float16_lut_outputs(ctx, prefix, bits_list) |
| actual_int = bits_to_int(actual_bits) |
| expected_int, expected_nan = float16_expected_bits_unary(op, a_bits) |
| ok = float16_is_nan_bits(actual_int) if expected_nan else actual_int == expected_int |
| total += 1 |
| if ok: |
| passed += 1 |
| elif len(failures) < 8: |
| failures.append({ |
| "input": hex(a_bits), |
| "actual": hex(actual_int), |
| "expected": hex(expected_int), |
| }) |
| results.append(TestResult(prefix, passed, total, failures)) |
|
|
| return results |
|
|
|
|
| def test_float16_domain_flags(ctx: EvalContext) -> List[TestResult]: |
| """Test float16 domain flag outputs.""" |
| results: List[TestResult] = [] |
| rng = random.Random(1337) |
| values = build_float16_values(rng, 256) |
| ops = [ |
| ("float16.sqrt", "sqrt"), |
| ("float16.rsqrt", "rsqrt"), |
| ("float16.ln", "ln"), |
| ("float16.log2", "log2"), |
| ("float16.log10", "log10"), |
| ("float16.asin", "asin"), |
| ("float16.acos", "acos"), |
| ("float16.asin_deg", "asin_deg"), |
| ("float16.acos_deg", "acos_deg"), |
| ] |
| for prefix, op in ops: |
| if f"{prefix}.domain.weight" not in ctx.tensors: |
| continue |
| passed, total = 0, 0 |
| failures: List[Dict[str, Any]] = [] |
| for a_bits in values: |
| bits_list = [float((a_bits >> i) & 1) for i in range(16)] |
| actual = eval_float16_lut_flag(ctx, prefix, bits_list) |
| expected = float16_expected_domain(op, a_bits) |
| total += 1 |
| if int(actual) == expected: |
| passed += 1 |
| elif len(failures) < 8: |
| failures.append({ |
| "input": hex(a_bits), |
| "actual": int(actual), |
| "expected": expected, |
| }) |
| results.append(TestResult(f"{prefix}.domain", passed, total, failures)) |
| return results |
|
|
|
|
| def test_float16_checked_outputs(ctx: EvalContext) -> List[TestResult]: |
| """Test checked outputs that force NaN on domain errors.""" |
| results: List[TestResult] = [] |
| rng = random.Random(1337) |
| values = build_float16_values(rng, 256) |
| ops = [ |
| ("float16.sqrt", "sqrt"), |
| ("float16.rsqrt", "rsqrt"), |
| ("float16.ln", "ln"), |
| ("float16.log2", "log2"), |
| ("float16.log10", "log10"), |
| ("float16.asin", "asin"), |
| ("float16.acos", "acos"), |
| ("float16.asin_deg", "asin_deg"), |
| ("float16.acos_deg", "acos_deg"), |
| ] |
| for prefix, op in ops: |
| if f"{prefix}.checked_out0.weight" not in ctx.tensors: |
| continue |
| passed, total = 0, 0 |
| failures: List[Dict[str, Any]] = [] |
| for a_bits in values: |
| bits_list = [float((a_bits >> i) & 1) for i in range(16)] |
| raw_bits = eval_float16_lut_outputs(ctx, prefix, bits_list) |
| domain = eval_float16_lut_flag(ctx, prefix, bits_list) |
| checked_bits: List[float] = [] |
| nan_bits = 0x7E00 |
| for i in range(16): |
| nan_bit = (nan_bits >> i) & 1 |
| if nan_bit: |
| checked = 1.0 if (raw_bits[i] >= 0.5 or domain >= 0.5) else 0.0 |
| else: |
| checked = 1.0 if (raw_bits[i] >= 0.5 and domain < 0.5) else 0.0 |
| checked_bits.append(checked) |
| gate = f"{prefix}.checked_out{i}" |
| for suffix in (".weight", ".bias", ".inputs"): |
| key = gate + suffix |
| if key in ctx.tensors: |
| ctx.tested_tensors.add(key) |
| actual_int = bits_to_int(checked_bits) |
| if float16_expected_domain(op, a_bits): |
| expected_int = 0x7E00 |
| else: |
| expected_int, _ = float16_expected_bits_unary(op, a_bits) |
| total += 1 |
| if actual_int == expected_int: |
| passed += 1 |
| elif len(failures) < 8: |
| failures.append({ |
| "input": hex(a_bits), |
| "actual": hex(actual_int), |
| "expected": hex(expected_int), |
| }) |
| results.append(TestResult(f"{prefix}.checked_out", passed, total, failures)) |
| return results |
|
|
|
|
| def test_float16_pow(ctx: EvalContext) -> List[TestResult]: |
| """Test float16.pow (defined as exp(b * ln(a))).""" |
| results: List[TestResult] = [] |
| if f"float16.pow.out0.weight" not in ctx.tensors: |
| return results |
|
|
| rng = random.Random(1337) |
| pairs = build_float16_pairs(rng, 512) |
| mul_prefix = "float16.pow.mul" |
| mul_gates = sorted([g for g in ctx.gates if g.startswith(mul_prefix + ".")]) |
|
|
| passed, total = 0, 0 |
| failures: List[Dict[str, Any]] = [] |
| for a_bits, b_bits in pairs: |
| a_list = [float((a_bits >> i) & 1) for i in range(16)] |
| b_list = [float((b_bits >> i) & 1) for i in range(16)] |
| |
| ln_bits = eval_float16_lut_outputs(ctx, "float16.pow.ln", a_list, match_prefix="float16.pow.ln") |
|
|
| |
| signals: Dict[int, float] = {} |
| if "#0" in ctx.name_to_id: |
| signals[ctx.name_to_id["#0"]] = 0.0 |
| if "#1" in ctx.name_to_id: |
| signals[ctx.name_to_id["#1"]] = 1.0 |
| for i in range(16): |
| sid = ctx.name_to_id.get(f"float16.pow.$b[{i}]") |
| if sid is not None: |
| signals[sid] = float(b_list[i]) |
| for i in range(16): |
| sid = ctx.name_to_id.get(f"float16.pow.ln.out{i}") |
| if sid is not None: |
| signals[sid] = float(ln_bits[i]) |
|
|
| if mul_prefix not in ctx.topo_cache or len(ctx.topo_cache[mul_prefix]) != len(mul_gates): |
| ctx.topo_cache[mul_prefix] = topo_sort_gates(ctx, mul_gates) |
| evaluate_gates_in_order(ctx, signals, ctx.topo_cache[mul_prefix]) |
|
|
| mul_bits = [] |
| for i in range(16): |
| gate = f"{mul_prefix}.out{i}" |
| sid = ctx.name_to_id.get(gate) |
| if sid is None or sid not in signals: |
| raise RuntimeError(f"{mul_prefix}: missing output {gate}") |
| mul_bits.append(float(signals[sid])) |
|
|
| exp_bits = eval_float16_lut_outputs(ctx, "float16.pow.exp", mul_bits, match_prefix="float16.pow.exp") |
|
|
| |
| for i in range(16): |
| gate = f"float16.pow.out{i}" |
| for suffix in (".weight", ".bias", ".inputs"): |
| key = gate + suffix |
| if key in ctx.tensors: |
| ctx.tested_tensors.add(key) |
|
|
| actual_int = bits_to_int(exp_bits) |
| expected_int, expected_nan = float16_expected_bits_pow(a_bits, b_bits) |
| ok = float16_is_nan_bits(actual_int) if expected_nan else actual_int == expected_int |
| total += 1 |
| if ok: |
| passed += 1 |
| elif len(failures) < 8: |
| failures.append({ |
| "a": hex(a_bits), |
| "b": hex(b_bits), |
| "actual": hex(actual_int), |
| "expected": hex(expected_int), |
| }) |
|
|
| results.append(TestResult("float16.pow", passed, total, failures)) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| CATEGORIES = { |
| "boolean": ("Boolean Gates", test_boolean_gates), |
| "threshold": ("Threshold Gates", test_threshold_gates), |
| "clz": ("CLZ (Count Leading Zeros)", test_clz), |
| "adders": ("Arithmetic - Adders", test_adders), |
| "comparators": ("Arithmetic - Comparators", test_comparators), |
| "multiplier": ("Arithmetic - Multiplier", test_multiplier), |
| "divider": ("Arithmetic - Divider", test_divider), |
| "bitwise": ("Arithmetic - Bitwise", test_bitwise), |
| "modular": ("Modular Arithmetic", test_modular), |
| "combinational": ("Combinational Logic", test_combinational), |
| "orphan": ("Orphan/Selector Tensors", test_orphan_tensors), |
| "pattern": ("Pattern Recognition", test_pattern_recognition), |
| "float16_basic": ("Float16 - Basic", test_float16_basic), |
| "float32_basic": ("Float32 - Basic", test_float32_basic), |
| "float16_arith": ("Float16 - Arithmetic", test_float16_arithmetic), |
| "float16_conv": ("Float16 - Conversion", test_float16_conversion), |
| "float16_unary": ("Float16 - Unary LUT", test_float16_unary), |
| "float16_constants": ("Float16 - Constants", test_float16_constants), |
| "float16_domain": ("Float16 - Domain Flags", test_float16_domain_flags), |
| "float16_checked": ("Float16 - Checked Outputs", test_float16_checked_outputs), |
| "float16_pow": ("Float16 - Pow", test_float16_pow), |
| } |
|
|
|
|
| def run_tests(ctx: EvalContext, categories: Optional[List[str]] = None, |
| circuits: Optional[List[str]] = None) -> List[TestResult]: |
| """Run tests for specified categories/circuits.""" |
| all_results = [] |
|
|
| cats_to_run = categories if categories else list(CATEGORIES.keys()) |
|
|
| for cat_key in cats_to_run: |
| if cat_key not in CATEGORIES: |
| print(f"Warning: Unknown category '{cat_key}'") |
| continue |
|
|
| cat_name, test_fn = CATEGORIES[cat_key] |
| print(f"\n=== {cat_name.upper()} ===") |
|
|
| results = test_fn(ctx) |
|
|
| for r in results: |
| if circuits and not any(c in r.circuit for c in circuits): |
| continue |
|
|
| status = "[PASS]" if r.success else "[FAIL]" |
| print(f" {r.circuit}: {r.passed}/{r.total} {status}") |
| all_results.append(r) |
|
|
| return all_results |
|
|
|
|
| def print_summary(results: List[TestResult], ctx: EvalContext, |
| elapsed: float, verbose: bool = False): |
| """Print test summary.""" |
| total_passed = sum(r.passed for r in results) |
| total_tests = sum(r.total for r in results) |
|
|
| print("\n" + "=" * 60) |
| print("SUMMARY") |
| print("=" * 60) |
| print(f"Total: {total_passed}/{total_tests} ({100.0 * total_passed / total_tests:.4f}%)") |
| print(f"Time: {elapsed:.2f}s") |
|
|
| failed = [r for r in results if not r.success] |
| if failed: |
| print(f"\nFailed ({len(failed)}):") |
| for r in failed: |
| print(f" {r.circuit}: {r.passed}/{r.total}") |
| else: |
| print("\nAll circuits passed!") |
|
|
| |
| coverage = len(ctx.tested_tensors) / len(ctx.tensors) * 100 |
| print(f"\n" + "=" * 60) |
| print(f"TENSOR COVERAGE: {len(ctx.tested_tensors)}/{len(ctx.tensors)} ({coverage:.2f}%)") |
|
|
| if verbose: |
| untested = set(ctx.tensors.keys()) - ctx.tested_tensors |
| print(f"\nUntested tensors: {len(untested)}") |
| for t in sorted(untested)[:20]: |
| print(f" - {t}") |
| if len(untested) > 20: |
| print(f" ... and {len(untested) - 20} more") |
|
|
| |
| fitness = total_passed / total_tests if total_tests > 0 else 0 |
| print(f"\nFitness: {fitness:.6f}") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Unified evaluator for threshold-calculus circuits") |
| parser.add_argument("--model", default="./arithmetic.safetensors", help="Path to model file") |
| parser.add_argument("--circuit", action="append", help="Test specific circuit (can repeat)") |
| parser.add_argument("--json", "-j", action="store_true", help="Output JSON for CI") |
| parser.add_argument("--coverage", action="store_true", help="Show detailed coverage") |
| parser.add_argument("--inputs-coverage", action="store_true", help="Sweep all gates using .inputs tensors") |
| parser.add_argument("--list", "-l", action="store_true", help="List categories and exit") |
|
|
| args = parser.parse_args() |
|
|
| if args.list: |
| print("Available categories:") |
| for key, (name, _) in CATEGORIES.items(): |
| print(f" {key}: {name}") |
| return 0 |
|
|
| print(f"Loading model from {args.model}...") |
| tensors, gates, signals, name_to_id, id_to_name = load_model(args.model) |
|
|
| print(f"Loaded {len(tensors)} tensors, {len(gates)} gates, {len(signals)} signals") |
|
|
| ctx = EvalContext( |
| tensors=tensors, |
| gates=gates, |
| signals=signals, |
| name_to_id=name_to_id, |
| id_to_name=id_to_name, |
| verbose=True, |
| quick=False, |
| ) |
|
|
| start = time.time() |
| results = run_tests(ctx, categories=None, circuits=args.circuit) |
|
|
| inputs_coverage_sweep(ctx, seed=0, verbose=True, quiet=args.json) |
| elapsed = time.time() - start |
|
|
| if args.json: |
| output = { |
| "total_passed": sum(r.passed for r in results), |
| "total_tests": sum(r.total for r in results), |
| "elapsed": elapsed, |
| "coverage": len(ctx.tested_tensors) / len(tensors), |
| "results": [{"circuit": r.circuit, "passed": r.passed, "total": r.total} for r in results], |
| } |
| print(json.dumps(output, indent=2)) |
| else: |
| print_summary(results, ctx, elapsed, verbose=True) |
|
|
| |
| failed = [r for r in results if not r.success] |
| return 1 if failed else 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|