Download tests/test_adder.py from AEUPH/hdcstorage: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/AEUPH/hdcstorage/resolve/main/tests/test_adder.py
- Command line
-
hf download hf://AEUPH/hdcstorage/tests/test_adder.py
-
curl -L -o test_adder.py https://huggingface.co/AEUPH/hdcstorage/resolve/main/tests/test_adder.py
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() | |