SilverSight/tests/test_dna_codec.py
allaunthefox 5331d2cc4e feat(dna): unified theory — DNA encoding, epigenetic computation, logarithmic vector spaces
Derivation from first principles:

1. Hachimoji DNA encoding (8 bases, ASCII-ordered, monotone LUT)
2. Imaginary Semantic Time (observer-independent semantic axis)
3. Sieve observers with CRT reconciliation (mod ℓ projections)
4. Semantic mass (E - E_min, E_s = m · 8²)
5. Gap preservation theorem (cleanMerge_preservesGap from GraphRank.lean)
6. Epigenetic computation (bistability, spreading, memory, attractors)
7. Logarithmic vector spaces (Kritchevsky: log N is a geometric vector)
8. Uncomputability framework (baseless logarithm = truth, based = computation)

Epigenetic optimizer breaks the freeze point:
  n=20: 0.7s (brute: 0.3s)
  n=24: 1.5s (brute: FROZEN)
  n=30: 3.4s (brute: FROZEN)
  n=50: 23.9s (brute: FROZEN)

Files:
  docs/UNIFIED_THEORY.md — full theory derivation
  docs/HACHIMOJI_DNA_SYNTAX.md — formal syntax specification
  docs/EPIGENETIC_COMPUTATION.md — epigenetic optimizer
  docs/UNCOMPUTABILITY.md — logarithmic vector space framework
  docs/REDERIVATION.md — rederivation from first principles
  python/dna_*.py — implementation (codec, LUT, GPU, surface)
  tests/test_dna_*.py — 68 tests, all green

Build: N/A (Python + Lean documentation)
2026-06-23 02:18:16 +00:00

353 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
test_dna_codec.py — Tests for the Hachimoji DNA encoding/decoding layer
Test categories:
1. Base encoding roundtrip
2. Binary vector encoding
3. QUBO energy computation
4. Tm sort verification (the critical test)
5. SAM/FASTA export
"""
import json
import math
import os
import random
import sys
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "python"))
from dna_codec import (
BASE_TO_BITS,
BITS_TO_BASE,
HACHIMOJI_BASES,
decode_binary_vector,
decode_dna_to_bytes,
dna_to_greek,
encode_binary_vector,
encode_bytes_to_dna,
gc_content,
greek_to_dna,
melting_temperature,
qubo_energy,
raw_gc_content,
sequence_stats,
)
from dna_qubo_sort import (
QuboDnaCandidate,
candidates_to_fasta,
candidates_to_sam,
demo_max_cut,
demo_number_partition,
encode_qubo_candidate,
generate_all_candidates,
generate_random_candidates,
run_qubo_dna_sort,
sort_by_energy,
sort_by_gc,
sort_by_tm,
spearman_rank_correlation,
verify_tm_sort,
)
class TestBaseEncoding(unittest.TestCase):
"""§1: Base encoding roundtrip."""
def test_all_bases_present(self):
"""All 8 Hachimoji bases are defined."""
self.assertEqual(len(HACHIMOJI_BASES), 8)
self.assertEqual(len(BITS_TO_BASE), 8)
self.assertEqual(len(BASE_TO_BITS), 8)
def test_bijection(self):
"""BITS_TO_BASE and BASE_TO_BITS are inverses."""
for bits, base in BITS_TO_BASE.items():
self.assertEqual(BASE_TO_BITS[base], bits)
for base, bits in BASE_TO_BITS.items():
self.assertEqual(BITS_TO_BASE[bits], base)
def test_3bit_coverage(self):
"""All 3-bit values (0-7) map to a base."""
for i in range(8):
self.assertIn(i, BITS_TO_BASE)
self.assertEqual(len(BITS_TO_BASE[i]), 1) # single character
def test_greek_roundtrip(self):
"""Latin → Greek → Latin roundtrip."""
for base in HACHIMOJI_BASES:
greek = dna_to_greek(base)
back = greek_to_dna(greek)
self.assertEqual(back, base, f"Failed for {base}{greek}{back}")
class TestByteEncoding(unittest.TestCase):
"""§2: Byte → DNA encoding roundtrip."""
def test_single_byte(self):
"""Single byte encodes and decodes."""
for b in range(256):
data = bytes([b])
dna = encode_bytes_to_dna(data)
decoded = decode_dna_to_bytes(dna)
self.assertEqual(decoded[:1], data, f"Failed for byte {b}")
def test_empty(self):
"""Empty input produces empty output."""
dna = encode_bytes_to_dna(b"")
self.assertEqual(dna, "")
decoded = decode_dna_to_bytes("")
self.assertEqual(decoded, b"")
def test_known_sequence(self):
"""Known byte → DNA mapping."""
# 0x00 = 00000000 → 000 000 00(pad 0) → A A A (3 bases)
dna = encode_bytes_to_dna(b"\x00")
self.assertEqual(dna, "AAA")
# 0xFF = 11111111 → 111 111 11(pad 0) → Z Z P (3 bases)
dna = encode_bytes_to_dna(b"\xff")
self.assertEqual(dna[0], "Z") # 111
self.assertEqual(dna[1], "Z") # 111
self.assertEqual(dna[2], "P") # 110 (padded with 0)
# 0b11010001 = 110 100 01(pad 0) = 110 100 010 → P B G
dna = encode_bytes_to_dna(b"\xd1")
self.assertEqual(dna[0], "P") # 110 = 6
self.assertEqual(dna[1], "B") # 100 = 4
self.assertEqual(dna[2], "G") # 010 = 2
def test_random_roundtrip(self):
"""Random bytes roundtrip through DNA encoding."""
rng = random.Random(42)
for _ in range(100):
length = rng.randint(1, 50)
data = bytes(rng.randint(0, 255) for _ in range(length))
dna = encode_bytes_to_dna(data)
decoded = decode_dna_to_bytes(dna)
# May have padding zeros at the end
self.assertEqual(decoded[:length], data)
def test_invalid_base_raises(self):
"""Invalid base character raises ValueError."""
with self.assertRaises(ValueError):
decode_dna_to_bytes("AXG")
class TestBinaryVectorEncoding(unittest.TestCase):
"""§3: Binary vector encoding."""
def test_all_zeros(self):
"""All-zero vector → all A's (3 bases per var by default)."""
x = [0, 0, 0, 0]
seq = encode_binary_vector(x)
self.assertEqual(seq, "A" * 12) # 4 vars × 3 bases = 12
def test_all_ones(self):
"""All-one vector → all P's (3 bases per var by default)."""
x = [1, 1, 1, 1]
seq = encode_binary_vector(x)
self.assertEqual(seq, "P" * 12) # 4 vars × 3 bases = 12
def test_mixed(self):
"""Mixed vector encodes correctly (3 bases per var)."""
x = [0, 1, 0, 1]
seq = encode_binary_vector(x)
self.assertEqual(seq, "AAAPPPAAAPPP") # 0→AAA, 1→PPP, 0→AAA, 1→PPP
def test_single_base_per_var(self):
"""bits_per_var=1 gives one base per variable."""
x = [0, 1, 0, 1]
seq = encode_binary_vector(x, bits_per_var=1)
self.assertEqual(seq, "APAP")
def test_roundtrip(self):
"""Binary vector roundtrip."""
for _ in range(50):
rng = random.Random(42)
x = [rng.randint(0, 1) for _ in range(20)]
seq = encode_binary_vector(x)
decoded = decode_binary_vector(seq)
self.assertEqual(decoded, x)
class TestMeltingTemperature(unittest.TestCase):
"""§4: Melting temperature model."""
def test_empty(self):
"""Empty sequence has Tm = 0."""
self.assertEqual(melting_temperature(""), 0.0)
def test_all_a_low_tm(self):
"""All-A sequence has low Tm."""
tm_a = melting_temperature("A" * 10)
tm_g = melting_temperature("G" * 10)
self.assertLess(tm_a, tm_g)
def test_gc_richer_higher_tm(self):
"""GC-rich sequences have higher Tm than AT-rich."""
at_rich = "AATT" * 10
gc_rich = "GGCC" * 10
self.assertLess(melting_temperature(at_rich), melting_temperature(gc_rich))
def test_synthetic_bases_intermediate(self):
"""Synthetic bases have intermediate Tm values."""
tm_a = TM_CONTRIBUTION["A"]
tm_g = TM_CONTRIBUTION["G"]
tm_p = TM_CONTRIBUTION["P"]
self.assertLess(tm_a, tm_p)
self.assertLess(tm_p, tm_g + 1) # P is between A and G
def test_gc_content_range(self):
"""GC content is in [0, 1]."""
for seq in ["AAAA", "GGGG", "ATGC", "PPZZ", ""]:
gc = gc_content(seq)
self.assertGreaterEqual(gc, 0.0)
self.assertLessEqual(gc, 1.0)
def test_tm_monotone_with_ones(self):
"""Tm is monotonically related to number of 1s in binary vector."""
# For encode_binary_vector: 0→A (Tm=2), 1→P (Tm=3.5)
# More 1s = higher Tm
for n_ones in range(10):
x = [1] * n_ones + [0] * (10 - n_ones)
seq = encode_binary_vector(x)
tm = melting_temperature(seq)
if n_ones > 0:
x_prev = [1] * (n_ones - 1) + [0] * (11 - n_ones)
seq_prev = encode_binary_vector(x_prev)
tm_prev = melting_temperature(seq_prev)
self.assertGreaterEqual(tm, tm_prev)
# Need TM_CONTRIBUTION for the test
from dna_codec import TM_CONTRIBUTION
class TestQuboEnergy(unittest.TestCase):
"""§5: QUBO energy computation."""
def test_zero_matrix(self):
"""Zero matrix → zero energy for any x."""
Q = [[0.0] * 4 for _ in range(4)]
self.assertEqual(qubo_energy([1, 0, 1, 0], Q), 0.0)
def test_identity_matrix(self):
"""Identity matrix → energy = number of 1s."""
n = 5
Q = [[1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
for k in range(n + 1):
x = [1] * k + [0] * (n - k)
self.assertAlmostEqual(qubo_energy(x, Q), float(k))
def test_known_qubo(self):
"""Known QUBO with hand-computed energy."""
# Q = [[1, -1], [-1, 1]]
# x = [0, 0] → E = 0
# x = [1, 0] → E = 1
# x = [0, 1] → E = 1
# x = [1, 1] → E = 0
Q = [[1.0, -1.0], [-1.0, 1.0]]
self.assertAlmostEqual(qubo_energy([0, 0], Q), 0.0)
self.assertAlmostEqual(qubo_energy([1, 0], Q), 1.0)
self.assertAlmostEqual(qubo_energy([0, 1], Q), 1.0)
self.assertAlmostEqual(qubo_energy([1, 1], Q), 0.0)
class TestTmSortVerification(unittest.TestCase):
"""§6: THE CRITICAL TEST — does Tm sorting = energy sorting?"""
def test_diagonal_qubo_perfect_correlation(self):
"""For diagonal Q with positive entries, Tm sort = energy sort exactly.
This is the ideal case: E(x) = Σ Q_ii * x_i, and Tm ∝ Σ x_i.
When all Q_ii are equal, Tm sort is identical to energy sort.
"""
n = 6
Q = [[3.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
result = verify_tm_sort(Q, n)
self.assertTrue(result.tm_sort_matches_energy)
self.assertGreater(result.rank_correlation, 0.99)
def test_uniform_qubo_rank_correlation(self):
"""For uniform Q, rank correlation should be very high."""
n = 8
Q = [[2.0 if i == j else 0.5 for j in range(n)] for i in range(n)]
result = verify_tm_sort(Q, n)
self.assertGreater(result.rank_correlation, 0.8)
def test_max_cut_sort(self):
"""Max-Cut QUBO: Tm sort should find a low-energy solution."""
Q = demo_max_cut(6, seed=42)
result = verify_tm_sort(Q, 6)
# Tm-optimal should be in the top-k by energy
self.assertGreater(result.top_k_agreement, 0)
def test_negative_entries_degrade_correlation(self):
"""Negative Q entries break the Tm-energy correlation.
This is expected: Tm encoding assumes non-negative contribution per 1.
Negative Q entries mean more 1s can LOWER energy, inverting the sort.
"""
n = 4
Q = [[-3.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
result = verify_tm_sort(Q, n)
# Correlation should be negative (inverted!)
self.assertLess(result.rank_correlation, 0.5)
class TestSamFastaExport(unittest.TestCase):
"""§7: SAM and FASTA export."""
def test_sam_format(self):
"""SAM output is valid-ish format."""
x = [1, 0, 1]
Q = [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]
cand = encode_qubo_candidate(x, Q)
sam = candidates_to_sam([cand], "test")
lines = sam.strip().split("\n")
# Header lines start with @
self.assertTrue(lines[0].startswith("@HD"))
self.assertTrue(lines[1].startswith("@PG"))
# Data line (3rd line after @CO comment)
data_line = [l for l in lines if not l.startswith("@")][0]
self.assertIn("TM:f:", data_line)
self.assertIn("EN:f:", data_line)
def test_fasta_format(self):
"""FASTA output starts with >."""
x = [1, 0, 1]
Q = [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]
cand = encode_qubo_candidate(x, Q)
fasta = candidates_to_fasta([cand])
lines = fasta.strip().split("\n")
self.assertTrue(lines[0].startswith(">"))
# Second line is sequence
self.assertTrue(all(c in HACHIMOJI_BASES for c in lines[1]))
class TestSequenceStats(unittest.TestCase):
"""§8: Sequence statistics."""
def test_stats_keys(self):
"""sequence_stats returns expected keys."""
stats = sequence_stats("ATGCBSPZ")
self.assertIn("length", stats)
self.assertIn("gc_content", stats)
self.assertIn("melting_temperature", stats)
self.assertIn("base_counts", stats)
self.assertIn("greek", stats)
def test_stats_length(self):
"""Length is correct."""
self.assertEqual(sequence_stats("ATGC")["length"], 4)
self.assertEqual(sequence_stats("")["length"], 0)
if __name__ == "__main__":
unittest.main(verbosity=2)