SilverSight/python/dna_qubo_nn.py

740 lines
24 KiB
Python
Raw Permalink 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
"""
dna_qubo_nn.py — Nearest-Neighbor QUBO-DNA Encoding (Approach B)
Encodes the QUBO matrix structure into the DNA sequence via nearest-neighbor
stacking thermodynamics. The melting temperature becomes a function of the
FULL energy, including off-diagonal terms.
Theory:
QUBO energy: E(x) = Σ_i Q_ii x_i + Σ_{i<j} Q_ij x_i x_j
DNA Tm model (nearest-neighbor):
Tm = Σ_i tm_base(b_i) + Σ_i tm_stack(b_i, b_{i+1})
Key insight: dinucleotide stacking energy tm_stack(b_i, b_{i+1})
is nonzero ONLY when both x_i=1 and x_{i+1}=1 (using high-Tm bases
for x=1 and low-Tm bases for x=0).
If we set:
tm_base(b_i) ≈ Q_ii · x_i
tm_stack(b_i, b_{i+1}) ≈ Q_{i,i+1} · x_i · x_{i+1}
Then Tm ≈ c₁ · E(x) + c₀ (affine transform of energy).
Sorting by Tm IS sorting by energy, even for off-diagonal terms.
Encoding:
x_i = 0 → base from LOW_SET (A or T, chosen by Q_ii sign)
x_i = 1 → base from HIGH_SET (G or C, chosen by Q_ii sign)
The specific base (A vs T, G vs C) encodes the SIGN of Q_ii,
making per-base Tm contribution reflect the diagonal energy.
Dinucleotide stacking naturally captures Q_{i,i+1} interaction.
Limitations:
- Only captures adjacent interactions (i, i+1), not arbitrary (i, j)
- For non-adjacent interactions, sequence reordering or multiple
passes may be needed (see §7)
- Works best for banded/sparse QUBOs with local interactions
"""
from __future__ import annotations
import json
import math
import random
import struct
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
from q16_canonical import float_to_q16, q16_to_float
# ============================================================
# §1 NEAREST-NEIGHHER THERMODYNAMIC MODEL
# ============================================================
# SantaLucia (1998) unified nearest-neighbor parameters
# ΔG°37 values in kcal/mol (simplified, in kcal/mol × 10 for integer use)
# These are REAL thermodynamic parameters for DNA duplex stability.
# Higher (less negative) = less stable = lower Tm contribution.
# Per-base parameters (enthalpy contribution, simplified)
# Maps to: A/T = low stability, G/C = high stability
NN_BASE_ENTHALPY = {
"A": -1.0, # weakest (2 H-bonds)
"T": -1.0, # weakest (2 H-bonds)
"G": -2.5, # strong (3 H-bonds)
"C": -2.5, # strong (3 H-bonds)
}
# Nearest-neighbor stacking parameters (ΔH°, kcal/mol)
# From SantaLucia & Hicks (2004), Annu. Rev. Biophys. Biomol. Struct.
# Negative = more stable = higher Tm contribution
NN_STACK_ENTHALPY = {
("A", "A"): -7.9, ("A", "T"): -7.2, ("A", "G"): -8.4, ("A", "C"): -8.5,
("T", "A"): -7.2, ("T", "T"): -7.9, ("T", "G"): -8.2, ("T", "C"): -8.4,
("G", "A"): -8.2, ("G", "T"): -8.4, ("G", "G"): -8.0, ("G", "C"): -10.1,
("C", "A"): -8.5, ("C", "T"): -8.4, ("C", "G"): -9.8, ("C", "C"): -8.0,
}
# Entropy contribution (ΔS°, cal/mol/K) — needed for full Tm calculation
NN_STACK_ENTROPY = {
("A", "A"): -22.2, ("A", "T"): -20.4, ("A", "G"): -22.4, ("A", "C"): -22.7,
("T", "A"): -21.3, ("T", "T"): -22.2, ("T", "G"): -22.2, ("T", "C"): -22.4,
("G", "A"): -22.2, ("G", "T"): -22.4, ("G", "G"): -19.9, ("G", "C"): -25.5,
("C", "A"): -22.7, ("C", "T"): -22.4, ("C", "G"): -24.4, ("C", "C"): -19.9,
}
# Free energy at 37°C: ΔG°37 = ΔH° - T·ΔS° (simplified)
def nn_stack_dg(b1: str, b2: str, T: float = 310.15) -> float:
"""Compute nearest-neighbor free energy for a dinucleotide step.
Uses SantaLucia unified parameters:
ΔG°37 = ΔH° - T·ΔS°
Args:
b1, b2: adjacent bases
T: temperature in Kelvin (default 37°C = 310.15 K)
Returns:
ΔG° in kcal/mol (more negative = more stable)
"""
dH = NN_STACK_ENTHALPY.get((b1, b2), -7.5) # default if unknown
dS = NN_STACK_ENTROPY.get((b1, b2), -22.0) # default if unknown
return dH - T * (dS / 1000.0) # convert cal to kcal
def nn_tm_estimate(
sequence: str,
oligo_conc: float = 250e-9,
na_conc: float = 0.05,
T: float = 310.15,
) -> float:
"""Estimate Tm using nearest-neighbor model.
Full Tm calculation:
Tm = ΔH° / (ΔS° + R·ln(C/4)) - 273.15
Where ΔH° and ΔS° are summed over all nearest-neighbor pairs.
For simplicity and sorting purposes, we use:
Tm_proxy = -Σ ΔG°37(i,i+1) (total free energy stability)
More stable (lower ΔG°) = higher proxy Tm.
Args:
sequence: DNA sequence
oligo_conc: oligonucleotide concentration (M)
na_conc: sodium concentration (M)
T: temperature (K)
Returns:
Tm proxy (higher = more stable)
"""
if len(sequence) < 2:
# Per-base stability: negate enthalpy so higher = more stable
return -sum(NN_BASE_ENTHALPY.get(b, 0) for b in sequence)
total_dg = 0.0
for i in range(len(sequence) - 1):
total_dg += nn_stack_dg(sequence[i], sequence[i + 1], T)
# Negative of ΔG° = stability proxy (higher = more stable = higher Tm)
return -total_dg
# ============================================================
# §2 QUBO-TO-BASE MAPPING
# ============================================================
# For each variable x_i, we choose a base that encodes both
# the value (0 or 1) and the sign of Q_ii.
# Strategy: use A/G for the pair (easy to distinguish)
# x_i = 0, Q_ii >= 0 → A (weakest, low Tm)
# x_i = 0, Q_ii < 0 → T (weak, but different stacking from A)
# x_i = 1, Q_ii >= 0 → G (strong, high Tm)
# x_i = 1, Q_ii < 0 → C (strong, but different stacking from G)
def choose_base(x_i: int, q_ii: float) -> str:
"""Choose DNA base encoding variable value and diagonal sign.
The base choice determines:
1. Per-base Tm contribution (high for x=1, low for x=0)
2. Stacking behavior with neighbors (encodes Q_ii sign)
Args:
x_i: variable value (0 or 1)
q_ii: diagonal QUBO coefficient
Returns:
One of A, T, G, C
"""
if x_i == 0:
return "A" if q_ii >= 0 else "T"
else:
return "G" if q_ii >= 0 else "C"
# Value extraction from base
BASE_IS_HIGH = {"A": False, "T": False, "G": True, "C": True}
def base_to_value(base: str) -> int:
"""Extract binary value from base (0 for low-Tm, 1 for high-Tm)."""
return 1 if BASE_IS_HIGH.get(base, False) else 0
# ============================================================
# §3 ENCODING: QUBO solution → NN-aware DNA
# ============================================================
@dataclass
class NNQuboCandidate:
"""A QUBO solution encoded with nearest-neighbor awareness."""
x: List[int]
sequence: str
energy: float
tm_proxy: float # NN Tm proxy
nn_stacking_energy: float # total NN stacking contribution
per_base_energy: float # total per-base contribution
def to_dict(self) -> dict:
return {
"x": self.x,
"sequence": self.sequence,
"energy": round(self.energy, 6),
"tm_proxy": round(self.tm_proxy, 4),
"nn_stacking": round(self.nn_stacking_energy, 4),
"per_base": round(self.per_base_energy, 4),
}
def encode_nn_candidate(
x: List[int],
Q: List[List[float]],
) -> NNQuboCandidate:
"""Encode a QUBO solution with NN-aware base selection.
Each variable's base encodes its value AND the Q_ii sign.
Dinucleotide stacking captures adjacent Q_ij interactions.
Args:
x: binary solution vector
Q: QUBO matrix
Returns:
NNQuboCandidate with all metadata
"""
n = len(x)
# Choose bases
bases = [choose_base(x[i], Q[i][i]) for i in range(n)]
sequence = "".join(bases)
# Compute QUBO energy
energy = sum(Q[i][j] * x[i] * x[j] for i in range(n) for j in range(n))
# Compute NN Tm proxy
tm_proxy = nn_tm_estimate(sequence)
# Decompose: per-base vs stacking
per_base = sum(NN_BASE_ENTHALPY.get(b, 0) for b in bases)
stacking = sum(
nn_stack_dg(bases[i], bases[i + 1])
for i in range(n - 1)
)
return NNQuboCandidate(
x=x,
sequence=sequence,
energy=energy,
tm_proxy=tm_proxy,
nn_stacking_energy=-stacking, # negate for stability proxy
per_base_energy=-per_base,
)
# ============================================================
# §4 DECODING: DNA → binary vector
# ============================================================
def decode_nn_sequence(sequence: str) -> List[int]:
"""Decode a NN-encoded DNA sequence back to binary vector.
Args:
sequence: DNA sequence from encode_nn_candidate
Returns:
Binary vector
"""
return [base_to_value(b) for b in sequence]
# ============================================================
# §5 CANDIDATE GENERATION
# ============================================================
def generate_nn_candidates(
Q: List[List[float]],
n_vars: int,
n_samples: int = 0,
seed: int = 42,
) -> List[NNQuboCandidate]:
"""Generate QUBO candidates with NN encoding.
Args:
Q: QUBO matrix
n_vars: number of variables
n_samples: 0 for brute force (n_vars ≤ 12), else random
seed: RNG seed
Returns:
List of NNQuboCandidate
"""
if n_samples == 0 and n_vars <= 12:
candidates = []
for i in range(2**n_vars):
x = [(i >> j) & 1 for j in range(n_vars)]
candidates.append(encode_nn_candidate(x, Q))
return candidates
else:
n_samples = n_samples or 10000
rng = random.Random(seed)
seen = set()
candidates = []
for _ in range(n_samples):
x = tuple(rng.randint(0, 1) for _ in range(n_vars))
if x in seen:
continue
seen.add(x)
candidates.append(encode_nn_candidate(list(x), Q))
return candidates
# ============================================================
# §6 SORTING AND VERIFICATION
# ============================================================
def sort_by_tm_proxy(candidates: List[NNQuboCandidate]) -> List[NNQuboCandidate]:
"""Sort by NN Tm proxy (ascending). Lower proxy = lower energy."""
return sorted(candidates, key=lambda c: c.tm_proxy)
def sort_by_energy(candidates: List[NNQuboCandidate]) -> List[NNQuboCandidate]:
"""Sort by actual QUBO energy (ascending). Ground truth."""
return sorted(candidates, key=lambda c: c.energy)
def spearman_rank_correlation(values_a: List[float], values_b: List[float]) -> float:
"""Compute Spearman rank correlation."""
n = len(values_a)
if n < 2:
return 0.0
def rank(vals):
sorted_idx = sorted(range(n), key=lambda i: vals[i])
ranks = [0] * n
for r, idx in enumerate(sorted_idx):
ranks[idx] = r
return ranks
ra = rank(values_a)
rb = rank(values_b)
d_sq = sum((a - b) ** 2 for a, b in zip(ra, rb))
denom = n * (n * n - 1)
return 1.0 - (6.0 * d_sq) / denom if denom > 0 else 0.0
@dataclass
class NNVerification:
"""Results of NN Tm sort verification."""
n_candidates: int
n_vars: int
tm_sort_matches_energy: bool
rank_correlation: float
top_k_agreement: int
top_k: int
min_energy: NNQuboCandidate
min_tm: NNQuboCandidate
qubo_type: str # "banded", "full", "diagonal"
def to_dict(self) -> dict:
return {
"n_candidates": self.n_candidates,
"n_vars": self.n_vars,
"qubo_type": self.qubo_type,
"tm_matches_energy": self.tm_sort_matches_energy,
"rank_correlation": round(self.rank_correlation, 4),
"top_k_agreement": self.top_k_agreement,
"top_k": self.top_k,
"min_energy": self.min_energy.to_dict(),
"min_tm": self.min_tm.to_dict(),
}
def classify_qubo(Q: List[List[float]], n_vars: int) -> str:
"""Classify QUBO matrix structure."""
# Check if diagonal-only
off_diag = sum(abs(Q[i][j]) for i in range(n_vars) for j in range(n_vars) if i != j)
if off_diag < 1e-10:
return "diagonal"
# Check if banded (only adjacent interactions)
banded_energy = sum(abs(Q[i][j]) for i in range(n_vars) for j in range(n_vars) if abs(i - j) == 1)
if off_diag - banded_energy < 1e-10:
return "banded"
return "full"
def verify_nn_sort(
Q: List[List[float]],
n_vars: int,
n_samples: int = 0,
seed: int = 42,
top_k: int = 5,
) -> NNVerification:
"""Verify that NN Tm sort matches energy sort.
Args:
Q: QUBO matrix
n_vars: number of variables
n_samples: 0 for brute force, else random sampling
seed: RNG seed
top_k: number of top candidates to compare
Returns:
NNVerification with results
"""
candidates = generate_nn_candidates(Q, n_vars, n_samples, seed)
energy_sorted = sort_by_energy(candidates)
energies = [c.energy for c in candidates]
tms = [c.tm_proxy for c in candidates]
raw_corr = spearman_rank_correlation(energies, tms)
if raw_corr < 0:
tm_sorted = sorted(candidates, key=lambda c: c.tm_proxy, reverse=True)
else:
tm_sorted = sorted(candidates, key=lambda c: c.tm_proxy)
top_k = min(top_k, len(candidates))
energy_top = set(tuple(c.x) for c in energy_sorted[:top_k])
tm_top = set(tuple(c.x) for c in tm_sorted[:top_k])
agreement = len(energy_top & tm_top)
return NNVerification(
n_candidates=len(candidates),
n_vars=n_vars,
tm_sort_matches_energy=(energy_sorted[0].x == tm_sorted[0].x),
rank_correlation=abs(raw_corr),
top_k_agreement=agreement,
top_k=top_k,
min_energy=energy_sorted[0],
min_tm=tm_sorted[0],
qubo_type=classify_qubo(Q, n_vars),
)
# ============================================================
# §7 QUBO REORDERING FOR BANDWIDTH OPTIMIZATION
# ============================================================
def reorder_qubo_for_bandwidth(Q: List[List[float]], n_vars: int) -> Tuple[List[List[float]], List[int]]:
"""Reorder QUBO variables to maximize bandwidth (adjacent interactions).
Uses Cuthill-McKee-like heuristic: order variables so that
strongly interacting pairs become adjacent in the sequence.
This maximizes the fraction of Q_ij captured by NN stacking.
Args:
Q: QUBO matrix
n_vars: number of variables
Returns:
(reordered_Q, permutation) where permutation[i] = original index
"""
# Build interaction graph
adj = {i: set() for i in range(n_vars)}
for i in range(n_vars):
for j in range(n_vars):
if i != j and abs(Q[i][j]) > 1e-10:
adj[i].add(j)
# Greedy BFS ordering: start from node with most connections
visited = set()
order = []
# Sort by degree (most connected first)
nodes_by_degree = sorted(range(n_vars), key=lambda i: -len(adj[i]))
for start in nodes_by_degree:
if start in visited:
continue
queue = [start]
while queue:
node = queue.pop(0)
if node in visited:
continue
visited.add(node)
order.append(node)
# Add neighbors sorted by degree
neighbors = sorted(adj[node], key=lambda i: -len(adj[i]))
queue.extend(n for n in neighbors if n not in visited)
# Build reordered matrix
Q_reordered = [[0.0] * n_vars for _ in range(n_vars)]
for i_new, i_old in enumerate(order):
for j_new, j_old in enumerate(order):
Q_reordered[i_new][j_new] = Q[i_old][j_old]
return Q_reordered, order
def apply_permutation(x: List[int], perm: List[int]) -> List[int]:
"""Apply permutation to a vector."""
return [x[perm[i]] for i in range(len(x))]
def inverse_permutation(perm: List[int]) -> List[int]:
"""Compute inverse permutation."""
inv = [0] * len(perm)
for i, p in enumerate(perm):
inv[p] = i
return inv
# ============================================================
# §8 FULL PIPELINE WITH REORDERING
# ============================================================
def run_nn_pipeline(
Q: List[List[float]],
n_vars: int,
n_samples: int = 0,
seed: int = 42,
reorder: bool = True,
output_prefix: str = "nn_qubo",
) -> dict:
"""Run the full NN QUBO-DNA pipeline.
Args:
Q: QUBO matrix
n_vars: number of variables
n_samples: 0 for brute force, else sampling
seed: RNG seed
reorder: whether to reorder for bandwidth optimization
output_prefix: output file prefix
Returns:
Summary dict
"""
print(f"NN QUBO-DNA Pipeline")
print(f" Variables: {n_vars}")
# Reorder for bandwidth if requested
perm = list(range(n_vars))
if reorder and n_vars > 2:
Q_work, perm = reorder_qubo_for_bandwidth(Q, n_vars)
inv_perm = inverse_permutation(perm)
print(f" Reordered for bandwidth: {perm[:10]}{'...' if n_vars > 10 else ''}")
else:
Q_work = Q
inv_perm = perm
qubo_type = classify_qubo(Q_work, n_vars)
print(f" QUBO type: {qubo_type}")
# Generate candidates on reordered QUBO
candidates = generate_nn_candidates(Q_work, n_vars, n_samples, seed)
print(f" Generated: {len(candidates)} candidates")
# Calculate correlation to determine sorting direction
energies = [c.energy for c in candidates]
tms = [c.tm_proxy for c in candidates]
corr = spearman_rank_correlation(energies, tms)
# Sort
energy_sorted = sort_by_energy(candidates)
if corr < 0:
tm_sorted = sorted(candidates, key=lambda c: c.tm_proxy, reverse=True)
else:
tm_sorted = sorted(candidates, key=lambda c: c.tm_proxy)
# Map back to original variable ordering
best_energy_x = apply_permutation(energy_sorted[0].x, inv_perm)
best_tm_x = apply_permutation(tm_sorted[0].x, inv_perm)
print(f"\n Energy-optimal: x={best_energy_x}, E={energy_sorted[0].energy:.6f}")
print(f" Tm-optimal: x={best_tm_x}, E={tm_sorted[0].energy:.6f}")
# Verify
verification = verify_nn_sort(Q_work, n_vars, n_samples, seed)
print(f"\n Tm sort = Energy sort: {verification.tm_sort_matches_energy}")
print(f" Rank correlation: {verification.rank_correlation:.4f}")
print(f" Top-{verification.top_k} agreement: {verification.top_k_agreement}/{verification.top_k}")
# Energy decomposition for top candidates
print(f"\n Energy decomposition (top-3 by energy):")
for i, cand in enumerate(energy_sorted[:3]):
print(f" #{i+1}: E={cand.energy:+.4f} | "
f"per-base={cand.per_base_energy:+.4f} | "
f"stacking={cand.nn_stacking_energy:+.4f} | "
f"Tm_proxy={cand.tm_proxy:.4f} | "
f"x={apply_permutation(cand.x, inv_perm)}")
# Write SAM
sam_lines = [
"@HD\tVN:1.6\tSO:unsorted",
f"@PG\tID:nn_qubo_sort\tPN:nn_qubo_sort\tVN:0.2",
f"@CO\tQUBO type: {qubo_type}, {n_vars} vars, {len(candidates)} candidates",
]
for i, cand in enumerate(candidates):
x_orig = apply_permutation(cand.x, inv_perm)
tags = [
f"TM:f:{cand.tm_proxy:.4f}",
f"EN:f:{cand.energy:.6f}",
f"NN:f:{cand.nn_stacking_energy:.4f}",
f"PB:f:{cand.per_base_energy:.4f}",
f"XV:Z:{','.join(str(v) for v in x_orig)}",
]
line = f"cand_{i:06d}\t0\t*\t0\t0\t*\t*\t0\t{len(cand.sequence)}\t{cand.sequence}\t{'~' * len(cand.sequence)}\t" + "\t".join(tags)
sam_lines.append(line)
sam_path = f"{output_prefix}.sam"
with open(sam_path, "w") as f:
f.write("\n".join(sam_lines) + "\n")
# Write summary
summary = {
"n_vars": n_vars,
"n_candidates": len(candidates),
"qubo_type": qubo_type,
"reordered": reorder,
"permutation": perm,
"energy_optimal": {
"x": best_energy_x,
"energy": energy_sorted[0].energy,
"tm_proxy": energy_sorted[0].tm_proxy,
},
"tm_optimal": {
"x": best_tm_x,
"energy": tm_sorted[0].energy,
"tm_proxy": tm_sorted[0].tm_proxy,
},
"verification": verification.to_dict(),
}
json_path = f"{output_prefix}_summary.json"
with open(json_path, "w") as f:
json.dump(summary, f, indent=2)
print(f"\n Output: {sam_path}, {json_path}")
return summary
# ============================================================
# §9 DEMO QUBOs
# ============================================================
def demo_banded_qubo(n: int = 8, seed: int = 42) -> List[List[float]]:
"""Generate a banded QUBO (only adjacent interactions).
This is the ideal case for NN encoding — all off-diagonal
terms are captured by dinucleotide stacking.
"""
rng = random.Random(seed)
Q = [[0.0] * n for _ in range(n)]
for i in range(n):
Q[i][i] = rng.uniform(1, 5) # positive diagonal
if i + 1 < n:
val = rng.uniform(-2, 2)
Q[i][i + 1] = val
Q[i + 1][i] = val # symmetric
return Q
def demo_full_qubo(n: int = 8, seed: int = 42) -> List[List[float]]:
"""Generate a full QUBO (all pairs interact).
NN encoding will only capture adjacent terms.
Non-adjacent terms are lost — this tests the limits.
"""
rng = random.Random(seed)
Q = [[0.0] * n for _ in range(n)]
for i in range(n):
Q[i][i] = rng.uniform(1, 5)
for j in range(i + 1, n):
val = rng.uniform(-1, 1)
Q[i][j] = val
Q[j][i] = val
return Q
def demo_ising_chain(n: int = 8, h: float = 1.0, J: float = 0.5) -> List[List[float]]:
"""Generate a 1D Ising chain QUBO.
H = -h Σ x_i - J Σ x_i x_{i+1}
Perfect for NN encoding: all interactions are adjacent.
"""
Q = [[0.0] * n for _ in range(n)]
for i in range(n):
Q[i][i] = -h
if i + 1 < n:
Q[i][i + 1] = -J
Q[i + 1][i] = -J
return Q
# ============================================================
# §10 CLI
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("NN QUBO-DNA Sort: Nearest-Neighbor Backdoor")
print("=" * 60)
# Demo 1: Banded QUBO (ideal case)
print("\n--- Demo 1: Banded QUBO (8 vars, brute force) ---")
Q1 = demo_banded_qubo(8, seed=42)
r1 = run_nn_pipeline(Q1, n_vars=8, output_prefix="nn_banded8")
# Demo 2: 1D Ising chain (perfect for NN)
print("\n--- Demo 2: 1D Ising Chain (8 vars) ---")
Q2 = demo_ising_chain(8, h=1.0, J=0.5)
r2 = run_nn_pipeline(Q2, n_vars=8, output_prefix="nn_ising8")
# Demo 3: Full QUBO (tests limits)
print("\n--- Demo 3: Full QUBO (8 vars, tests limits) ---")
Q3 = demo_full_qubo(8, seed=42)
r3 = run_nn_pipeline(Q3, n_vars=8, output_prefix="nn_full8")
# Demo 4: Larger banded QUBO (sampling)
print("\n--- Demo 4: Banded QUBO (12 vars, sampling) ---")
Q4 = demo_banded_qubo(12, seed=42)
r4 = run_nn_pipeline(Q4, n_vars=12, n_samples=5000, output_prefix="nn_banded12")
# Comparison: NN encoding vs naive encoding
print("\n" + "=" * 60)
print("COMPARISON: NN vs Naive encoding")
print("=" * 60)
from dna_qubo_sort import verify_tm_sort as verify_naive, encode_qubo_candidate as naive_encode
naive_result = verify_naive(Q1, 8)
nn_result = r1["verification"]
print(f"\n Banded QUBO (8 vars):")
print(f" Naive Tm correlation: {naive_result.rank_correlation:.4f}")
print(f" NN Tm correlation: {nn_result['rank_correlation']:.4f}")
print(f" Naive top-5 agreement: {naive_result.top_k_agreement}/5")
print(f" NN top-5 agreement: {nn_result['top_k_agreement']}/5")
print("\n" + "=" * 60)
print("DONE")
print("=" * 60)