mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
268 lines
9 KiB
Python
268 lines
9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
test_dna_nn.py — Tests for nearest-neighbor QUBO-DNA encoding
|
|
|
|
Tests the critical property: NN Tm sort = energy sort for banded QUBOs.
|
|
"""
|
|
|
|
import math
|
|
import os
|
|
import random
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "python"))
|
|
|
|
from dna_qubo_nn import (
|
|
NNQuboCandidate,
|
|
apply_permutation,
|
|
base_to_value,
|
|
choose_base,
|
|
classify_qubo,
|
|
decode_nn_sequence,
|
|
demo_banded_qubo,
|
|
demo_full_qubo,
|
|
demo_ising_chain,
|
|
encode_nn_candidate,
|
|
generate_nn_candidates,
|
|
inverse_permutation,
|
|
nn_stack_dg,
|
|
nn_tm_estimate,
|
|
reorder_qubo_for_bandwidth,
|
|
sort_by_energy,
|
|
sort_by_tm_proxy,
|
|
spearman_rank_correlation,
|
|
verify_nn_sort,
|
|
)
|
|
|
|
|
|
class TestBaseChoice(unittest.TestCase):
|
|
"""§1: Base selection encodes value + diagonal sign."""
|
|
|
|
def test_zero_positive_diagonal(self):
|
|
"""x=0, Q_ii >= 0 → A"""
|
|
self.assertEqual(choose_base(0, 3.0), "A")
|
|
self.assertEqual(choose_base(0, 0.0), "A")
|
|
|
|
def test_zero_negative_diagonal(self):
|
|
"""x=0, Q_ii < 0 → T"""
|
|
self.assertEqual(choose_base(0, -1.0), "T")
|
|
|
|
def test_one_positive_diagonal(self):
|
|
"""x=1, Q_ii >= 0 → G"""
|
|
self.assertEqual(choose_base(1, 3.0), "G")
|
|
|
|
def test_one_negative_diagonal(self):
|
|
"""x=1, Q_ii < 0 → C"""
|
|
self.assertEqual(choose_base(1, -1.0), "C")
|
|
|
|
def test_base_to_value(self):
|
|
"""Low-Tm bases → 0, high-Tm bases → 1."""
|
|
self.assertEqual(base_to_value("A"), 0)
|
|
self.assertEqual(base_to_value("T"), 0)
|
|
self.assertEqual(base_to_value("G"), 1)
|
|
self.assertEqual(base_to_value("C"), 1)
|
|
|
|
|
|
class TestNNThermodynamics(unittest.TestCase):
|
|
"""§2: Nearest-neighbor thermodynamic model."""
|
|
|
|
def test_gc_richer_higher_tm(self):
|
|
"""GC-rich sequences have higher Tm proxy than AT-rich."""
|
|
at_rich = "AATT" * 10
|
|
gc_rich = "GGCC" * 10
|
|
self.assertGreater(nn_tm_estimate(gc_rich), nn_tm_estimate(at_rich))
|
|
|
|
def test_stacking_energies_finite(self):
|
|
"""All dinucleotide stacking energies are finite."""
|
|
for b1 in "ATGC":
|
|
for b2 in "ATGC":
|
|
dg = nn_stack_dg(b1, b2)
|
|
self.assertTrue(math.isfinite(dg))
|
|
|
|
def test_gc_gc_most_stable(self):
|
|
"""GC/GC stacking is among the most stable."""
|
|
gc_gc = nn_stack_dg("G", "C")
|
|
at_at = nn_stack_dg("A", "T")
|
|
self.assertLess(gc_gc, at_at) # more negative = more stable
|
|
|
|
def test_empty_sequence(self):
|
|
"""Empty sequence has zero Tm."""
|
|
self.assertEqual(nn_tm_estimate(""), 0.0)
|
|
|
|
def test_single_base(self):
|
|
"""Single base has per-base Tm only."""
|
|
tm_g = nn_tm_estimate("G")
|
|
tm_a = nn_tm_estimate("A")
|
|
self.assertGreater(tm_g, tm_a)
|
|
|
|
|
|
class TestEncodeDecode(unittest.TestCase):
|
|
"""§3: NN encoding and decoding."""
|
|
|
|
def test_roundtrip(self):
|
|
"""Encode → decode roundtrip preserves binary vector."""
|
|
Q = [[3.0, 0, 0], [0, 2.0, 0], [0, 0, 1.0]]
|
|
for x in [[0, 0, 0], [1, 1, 1], [0, 1, 0], [1, 0, 1]]:
|
|
cand = encode_nn_candidate(x, Q)
|
|
decoded = decode_nn_sequence(cand.sequence)
|
|
self.assertEqual(decoded, x, f"Failed for x={x}")
|
|
|
|
def test_all_zeros(self):
|
|
"""All-zero vector → all low-Tm bases."""
|
|
n = 5
|
|
Q = [[2.0] * n for _ in range(n)]
|
|
x = [0] * n
|
|
cand = encode_nn_candidate(x, Q)
|
|
for base in cand.sequence:
|
|
self.assertIn(base, "AT")
|
|
|
|
def test_all_ones(self):
|
|
"""All-one vector → all high-Tm bases."""
|
|
n = 5
|
|
Q = [[2.0] * n for _ in range(n)]
|
|
x = [1] * n
|
|
cand = encode_nn_candidate(x, Q)
|
|
for base in cand.sequence:
|
|
self.assertIn(base, "GC")
|
|
|
|
def test_negative_diagonal_uses_different_bases(self):
|
|
"""Negative Q_ii uses T/C instead of A/G."""
|
|
Q_pos = [[3.0]]
|
|
Q_neg = [[-3.0]]
|
|
self.assertEqual(encode_nn_candidate([0], Q_pos).sequence, "A")
|
|
self.assertEqual(encode_nn_candidate([0], Q_neg).sequence, "T")
|
|
self.assertEqual(encode_nn_candidate([1], Q_pos).sequence, "G")
|
|
self.assertEqual(encode_nn_candidate([1], Q_neg).sequence, "C")
|
|
|
|
|
|
class TestTmSortBanded(unittest.TestCase):
|
|
"""§4: THE CRITICAL TEST — banded QUBOs."""
|
|
|
|
def test_diagonal_perfect(self):
|
|
"""Diagonal QUBO: Tm sort = energy sort exactly."""
|
|
n = 6
|
|
Q = [[3.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
|
|
result = verify_nn_sort(Q, n)
|
|
self.assertTrue(result.tm_sort_matches_energy)
|
|
self.assertGreater(result.rank_correlation, 0.9)
|
|
|
|
def test_banded_high_correlation(self):
|
|
"""Banded QUBO: Tm sort has high rank correlation."""
|
|
Q = demo_banded_qubo(8, seed=42)
|
|
result = verify_nn_sort(Q, 8)
|
|
self.assertGreater(result.rank_correlation, 0.35)
|
|
self.assertGreater(result.top_k_agreement, 0)
|
|
|
|
def test_ising_chain_perfect(self):
|
|
"""1D Ising chain: NN encoding captures all interactions."""
|
|
Q = demo_ising_chain(8, h=1.0, J=0.5)
|
|
result = verify_nn_sort(Q, 8)
|
|
# All interactions are adjacent → NN captures everything
|
|
self.assertGreater(result.rank_correlation, 0.9)
|
|
|
|
def test_ising_chain_optimal_match(self):
|
|
"""Ising chain: Tm-optimal is energy-optimal."""
|
|
Q = demo_ising_chain(6, h=1.0, J=0.5)
|
|
result = verify_nn_sort(Q, 6)
|
|
self.assertTrue(result.tm_sort_matches_energy)
|
|
|
|
|
|
class TestTmSortFull(unittest.TestCase):
|
|
"""§5: Full QUBO — tests the limits."""
|
|
|
|
def test_full_qubo_degraded(self):
|
|
"""Full QUBO: NN encoding loses non-adjacent interactions."""
|
|
Q = demo_full_qubo(8, seed=42)
|
|
result = verify_nn_sort(Q, 8)
|
|
# Should be worse than banded but still positive
|
|
self.assertGreater(result.rank_correlation, 0.0)
|
|
|
|
def test_full_qubo_still_finds_decent(self):
|
|
"""Full QUBO: Tm-optimal is at least in the top-k by energy."""
|
|
Q = demo_full_qubo(8, seed=42)
|
|
result = verify_nn_sort(Q, 8, top_k=10)
|
|
self.assertGreater(result.top_k_agreement, 0)
|
|
|
|
|
|
class TestBandwidthReordering(unittest.TestCase):
|
|
"""§6: QUBO reordering for bandwidth optimization."""
|
|
|
|
def test_identity_permutation(self):
|
|
"""Already-banded QUBO keeps similar ordering."""
|
|
Q = demo_banded_qubo(6, seed=42)
|
|
Q_reordered, perm = reorder_qubo_for_bandwidth(Q, 6)
|
|
# Should be similar (not necessarily identical)
|
|
self.assertEqual(len(perm), 6)
|
|
|
|
def test_permutation_inverse(self):
|
|
"""Permutation and inverse are correct."""
|
|
perm = [2, 0, 1, 3]
|
|
inv = inverse_permutation(perm)
|
|
for i in range(4):
|
|
self.assertEqual(perm[inv[i]], i)
|
|
|
|
def test_apply_permutation(self):
|
|
"""Apply permutation correctly reorders vector."""
|
|
x = [10, 20, 30, 40]
|
|
perm = [2, 0, 3, 1]
|
|
result = apply_permutation(x, perm)
|
|
self.assertEqual(result, [30, 10, 40, 20])
|
|
|
|
def test_reordering_preserves_energy(self):
|
|
"""Reordering doesn't change QUBO energies."""
|
|
Q = demo_full_qubo(6, seed=42)
|
|
Q_reord, perm = reorder_qubo_for_bandwidth(Q, 6)
|
|
inv = inverse_permutation(perm)
|
|
|
|
# Energy of a solution should be the same
|
|
x_orig = [1, 0, 1, 0, 1, 0]
|
|
x_reord = apply_permutation(x_orig, perm)
|
|
|
|
e_orig = sum(Q[i][j] * x_orig[i] * x_orig[j] for i in range(6) for j in range(6))
|
|
e_reord = sum(Q_reord[i][j] * x_reord[i] * x_reord[j] for i in range(6) for j in range(6))
|
|
|
|
self.assertAlmostEqual(e_orig, e_reord, places=10)
|
|
|
|
|
|
class TestQuboClassification(unittest.TestCase):
|
|
"""§7: QUBO type classification."""
|
|
|
|
def test_diagonal(self):
|
|
"""Diagonal matrix classified as 'diagonal'."""
|
|
Q = [[3.0, 0], [0, 2.0]]
|
|
self.assertEqual(classify_qubo(Q, 2), "diagonal")
|
|
|
|
def test_banded(self):
|
|
"""Tridiagonal matrix classified as 'banded'."""
|
|
Q = [[3.0, 1.0, 0], [1.0, 2.0, 0.5], [0, 0.5, 1.0]]
|
|
self.assertEqual(classify_qubo(Q, 3), "banded")
|
|
|
|
def test_full(self):
|
|
"""Full matrix classified as 'full'."""
|
|
Q = [[3.0, 1.0, 0.5], [1.0, 2.0, 0.3], [0.5, 0.3, 1.0]]
|
|
self.assertEqual(classify_qubo(Q, 3), "full")
|
|
|
|
|
|
class TestSpearman(unittest.TestCase):
|
|
"""§8: Rank correlation."""
|
|
|
|
def test_perfect_correlation(self):
|
|
"""Identical lists → correlation = 1.0."""
|
|
vals = [1.0, 2.0, 3.0, 4.0, 5.0]
|
|
self.assertAlmostEqual(spearman_rank_correlation(vals, vals), 1.0)
|
|
|
|
def test_inverse_correlation(self):
|
|
"""Reversed list → correlation = -1.0 (approximately)."""
|
|
vals = [1.0, 2.0, 3.0, 4.0, 5.0]
|
|
rev = list(reversed(vals))
|
|
corr = spearman_rank_correlation(vals, rev)
|
|
self.assertLess(corr, -0.9)
|
|
|
|
def test_empty(self):
|
|
"""Empty lists → 0.0."""
|
|
self.assertEqual(spearman_rank_correlation([], []), 0.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|