hdcstorage / tests /test_adder.py
AEUPH's picture
HDCSTORAGE: hyperdimensional computer, webOS and agentic MCP layer (stdlib-only Python)
ad474c9 verified
Raw History Blame Contribute Delete
10.5 kB
"""Tests for the hyperdimensional full adder.
Two of these are not ordinary arithmetic tests:
* ``TestGate.test_keys_are_not_parity`` is a regression test for a real bug.
XORing shared zero/one atoms collapsed the 8-row truth table onto 2 keys,
because ``1^1^1 == 1^0^0``. The fix was one atom pair per gate slot.
* ``TestNoDecode`` patches out ``hv_to_int`` inside the interpreter and runs
ADD/SUB, which is the only way to show the operands never leave the vector
domain: if any decode remained, the program would fault.
"""
import random
import sys
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from hdc.adder import AdderError, HyperAdder, wrap
from hdc.assembler import assemble
from hdc.hypervec import (
MAX_INT_BITS,
bind,
hv_bits_of_int,
hv_from_bits,
hv_to_bits,
hv_to_int,
int_to_hv,
sim,
)
from hdc.interpreter import Machine
from hdc.memory import HDCStorage
D = 2000
def build(source):
m = Machine(storage=HDCStorage(d=D), d=D)
m.load(assemble(source, d=D))
return m
class TestGate(unittest.TestCase):
def setUp(self):
self.adder = HyperAdder(d=D)
def test_table_has_eight_correct_rows(self):
self.assertEqual(len(self.adder.rows), 8)
for row in self.adder.rows:
total = row.a ^ row.b ^ row.c
carry = 1 if (row.a + row.b + row.c) >= 2 else 0
self.assertEqual((row.total, row.carry), (total, carry), row.render())
def test_rows_are_stored_in_hdc_storage(self):
for row in self.adder.rows:
self.assertIn(row.cell, self.adder.storage)
self.assertNotEqual(self.adder.storage.read(row.cell), 0)
def test_keys_are_not_parity(self):
"""Regression: a shared atom pair reduces every key to its parity."""
keys = [row.key for row in self.adder.rows]
self.assertEqual(len(set(keys)), 8, "truth-table keys collided")
worst = min(
sim(a, b, D) for i, a in enumerate(keys) for j, b in enumerate(keys) if i != j
)
self.assertLess(worst, 0.15, "row keys are not near-orthogonal")
def test_full_adder_all_eight_inputs(self):
for a in (0, 1):
for b in (0, 1):
for c in (0, 1):
total, carry, confidence = self.adder.full_adder(a, b, c)
self.assertEqual(total, a ^ b ^ c, (a, b, c))
self.assertEqual(carry, 1 if a + b + c >= 2 else 0, (a, b, c))
self.assertGreater(confidence, 0.3)
def test_separation_reports_real_numbers(self):
report = self.adder.separation()
self.assertEqual(report["rows"], 8)
self.assertEqual(report["table_cells"], 8)
self.assertLess(report["worst_cross_similarity"], 0.15)
class TestRipple(unittest.TestCase):
def setUp(self):
self.adder = HyperAdder(d=D)
def test_single_bit_carry_chain(self):
bits, stages = self.adder.add_bits([1, 1], [1, 0], 0)
# 1 + 3 = 4 -> 100
self.assertEqual(bits[:3], [0, 0, 1])
self.assertEqual([s.carry_out for s in stages[:3]], [1, 1, 0])
def test_carry_propagates_across_many_bits(self):
bits, _ = self.adder.add_bits(hv_bits_of_int(0x7FFFFFFF), hv_bits_of_int(1), 0)
self.assertEqual(hv_to_int(hv_from_bits(bits, D), D), wrap(0x7FFFFFFF + 1))
def test_add_matches_python(self):
rng = random.Random(7)
for _ in range(25):
a = rng.randrange(-(1 << 31), 1 << 31)
b = rng.randrange(-(1 << 31), 1 << 31)
self.assertEqual(self.adder.decode(self.adder.add(a, b)), wrap(a + b), (a, b))
def test_sub_matches_python(self):
rng = random.Random(8)
for _ in range(25):
a = rng.randrange(-(1 << 31), 1 << 31)
b = rng.randrange(-(1 << 31), 1 << 31)
self.assertEqual(self.adder.decode(self.adder.sub(a, b)), wrap(a - b), (a, b))
def test_known_values(self):
cases = [(0, 0, 0), (1, 1, 2), (7, 35, 42), (-7, 35, 28), (5, -11, -6),
(-1, 1, 0), (-2, -3, -5), (65535, 1, 65536)]
for a, b, expected in cases:
self.assertEqual(self.adder.decode(self.adder.add(a, b)), expected, (a, b))
def test_hv_to_hv_layer(self):
a, b = 1234, -5678
out = self.adder.add_hv(int_to_hv(a, D), int_to_hv(b, D))
self.assertEqual(self.adder.decode(out), wrap(a + b))
self.assertEqual(out, self.adder.add(a, b), "HV path differs from int path")
def test_sub_hv_layer(self):
a, b = 99, 1234
out = self.adder.sub_hv(int_to_hv(a, D), int_to_hv(b, D))
self.assertEqual(self.adder.decode(out), wrap(a - b))
def test_bit_extraction_matches_codec(self):
for n in (0, 1, -1, 42, -42, 0x55555555, -0x55555556, (1 << 31) - 1):
self.assertEqual(
hv_to_bits(int_to_hv(n, D), D), hv_bits_of_int(n), "value %d" % n
)
class TestNoDecode(unittest.TestCase):
"""Prove ADD/SUB execute without materialising an integer operand."""
def test_add_and_sub_without_hv_to_int(self):
m = build(
"""
ldi R0, 7
ldi R1, 35
add R2, R0, R1
sub R3, R0, R1
halt
"""
)
with mock.patch("hdc.interpreter.hv_to_int", side_effect=AssertionError("decoded")):
result = m.run()
self.assertEqual(result["status"], "halted")
self.assertEqual(m.adder.decode(m.regs[2]), 42)
self.assertEqual(m.adder.decode(m.regs[3]), -28)
def test_trace_reports_stage_count(self):
m = build("ldi R0, 2\n ldi R1, 3\n add R2, R0, R1\n halt")
m.run()
notes = [e.note for e in m.trace if e.op == "ADD"]
self.assertEqual(len(notes), 1)
self.assertIn("stages", notes[0])
self.assertIn("radix 8", notes[0])
self.assertEqual(len(m.adder.last_stages), 4)
class TestMachineIntegration(unittest.TestCase):
def test_program_arithmetic_through_circuit(self):
m = build(
"""
ldi R0, 21
ldi R1, 2
mul R2, R0, R1
add R3, R2, R0
sub R4, R3, R1
out R3
out R4
halt
"""
)
m.run()
self.assertEqual(m.output, ["63", "61"])
def test_adder_stages_are_counted(self):
m = build("ldi R0, 1\n ldi R1, 2\n add R2, R0, R1\n sub R3, R1, R0\n halt")
m.run()
per_op = (m.adder.width + m.adder.radix - 1) // m.adder.radix
self.assertEqual(m.adder.stages_executed, 2 * per_op)
def test_wraparound_is_hardware_like(self):
m = build("ldi R0, 2147483647\n ldi R1, 1\n add R2, R0, R1\n halt")
m.run()
self.assertEqual(m.adder.decode(m.regs[2]), -(1 << 31))
class TestSliceGates(unittest.TestCase):
"""Wide gates: fewer stages, bigger tables, lazy materialisation."""
def test_stage_counts_per_radix(self):
for radix, stages in ((1, 32), (2, 16), (4, 8), (8, 4)):
adder = HyperAdder(d=D, radix=radix)
bits, _ = adder.add_bits(hv_bits_of_int(1), hv_bits_of_int(1), 0)
self.assertEqual(len(adder.last_stages), stages, "radix %d" % radix)
self.assertEqual(adder.decode(hv_from_bits(bits, D)), 2)
def test_radix8_arithmetic(self):
adder = HyperAdder(d=D, radix=8)
report = adder.verify(20)
self.assertEqual(report["add_correct"], 20)
self.assertEqual(report["sub_correct"], 20)
self.assertEqual(report["hv_to_hv_correct"], 20)
def test_all_radices_agree(self):
cases = [(7, 35), (-7, 35), (-1, 1), ((1 << 31) - 1, 1), (-12345, -54321)]
for radix in (1, 2, 4, 8):
adder = HyperAdder(d=D, radix=radix)
for a, b in cases:
self.assertEqual(
adder.decode(adder.add(a, b)), wrap(a + b), (radix, a, b)
)
def test_byte_table_is_lazy(self):
adder = HyperAdder(d=D, radix=8)
self.assertEqual(len(adder.gate.rows), 0)
adder.add(1, 1)
# 131 072 rows exist in principle; four additions touch only a few.
self.assertEqual(adder.gate.row_count, 1 << 17)
self.assertLess(len(adder.gate.rows), 16)
self.assertFalse(adder.gate.eager)
def test_rows_materialise_into_storage(self):
adder = HyperAdder(d=D, radix=8)
adder.add(300, 400)
self.assertEqual(len(adder.gate.storage), len(adder.gate.rows))
for row in adder.gate.rows.values():
self.assertIn(row.cell, adder.gate.storage)
def test_gate_row_reads_back_as_built(self):
# evaluate() asserts internally that the decoded fields equal the row;
# this drives every combination through it for a 2-bit gate.
adder = HyperAdder(d=D, radix=2)
gate = adder.gate
for c in (0, 1):
for b in range(4):
for a in range(4):
total, carry, confidence = gate.evaluate(a, b, c)
self.assertEqual((total, carry), ((a + b + c) & 3, (a + b + c) >> 2))
self.assertGreater(confidence, 0.1)
def test_similarity_fallback_resolves_nearest_row(self):
adder = HyperAdder(d=D, radix=8)
adder.add(1, 1)
row, confidence = adder.gate.resolve_similar(0xFF, 0xFF, 1)
self.assertIsNotNone(row)
self.assertLessEqual(confidence, 1.0)
def test_machine_default_is_four_stages(self):
m = build("ldi R0, 1\n ldi R1, 2\n add R2, R0, R1\n halt")
self.assertEqual(m.adder.radix, 8)
m.run()
self.assertEqual(len(m.adder.last_stages), 4)
self.assertEqual(m.adder.decode(m.regs[2]), 3)
class TestCodecIsTwosComplement(unittest.TestCase):
def test_store_slots(self):
self.assertEqual(MAX_INT_BITS, 32)
def test_negative_values_are_dense(self):
# -1 sets every bit, which is why the codec threshold had to be tuned
# for 32-atom bundles rather than the old 8-member assumption.
self.assertEqual(sum(hv_bits_of_int(-1)), 32)
def test_range_enforced(self):
with self.assertRaises(ValueError):
int_to_hv(1 << 31, D)
with self.assertRaises(ValueError):
int_to_hv(-(1 << 31) - 1, D)
if __name__ == "__main__":
unittest.main()