mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
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)
353 lines
12 KiB
Python
353 lines
12 KiB
Python
#!/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)
|