#!/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)