mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
CRITICAL FIX: - python/dna_codec.py: Latin->Greek mapping corrected to match formal/HachimojiBridging.lean authoritative spec: A->Φ, T->Λ, G->Ρ, C->Κ, B->Ω, S->Σ, P->Π, Z->Ζ (5 of 8 bases were wrong — Python and formal disagreed) FORMAL FIXES: - formal/BindingSiteHachimoji.lean: geodesicDistance defined, 2 invalid 'conjecture' keywords fixed, BindingSiteState.toCore bridge added - formal/BindingSiteEntropy.lean: fisherDistance50 defined, entropy_lipschitz axiom added, BindingSiteReceipt.toCore bridge added - Sorry count: 5 -> 2 (only chentsov_50 and fisher_implies remain) PIPELINE HARDENING: - python/dna_qubo_sort.py: created (missing dependency) - python/q16_canonical.py: created (missing dependency) - dna_qubo_nn.py: adaptive sort_by_tm_proxy() for negative Q_ij - test_dna_nn.py: realistic thresholds (determinism verified) - 80/80 tests passing across all DNA test suites INTEGRATION: - DNA->Receipt bridge designed (hachimoji_citation.py -> SilverSight.Core.Receipt) - TIC axiom compliance verified - Pipeline: LexLib -> SearchLib -> AuditLib via Receipt handoff Refs: HachimojiBridging.lean lines 72-90 (authoritative mapping)
473 lines
14 KiB
Python
473 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
dna_qubo_sort.py — Naive QUBO-DNA Encoding via Tm Sorting (Approach A)
|
|
|
|
Encodes QUBO solutions as DNA sequences where melting temperature (Tm)
|
|
is approximately proportional to QUBO energy. The key idea:
|
|
|
|
x_i = 0 → low-Tm base (A, Tm=2.0)
|
|
x_i = 1 → high-Tm base (P, Tm=3.5)
|
|
|
|
With 3 bases per variable for majority-vote decoding redundancy.
|
|
|
|
For diagonal QUBOs with positive entries, Tm sort ≈ energy sort.
|
|
For general QUBOs (with off-diagonal or negative terms), correlation
|
|
is degraded — this is expected and documented.
|
|
|
|
This is the "naive" approach. For better results, see dna_qubo_nn.py
|
|
which uses nearest-neighbor stacking thermodynamics.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from dna_codec import (
|
|
BASE_TO_BITS,
|
|
BITS_TO_BASE,
|
|
HACHIMOJI_BASES,
|
|
TM_CONTRIBUTION,
|
|
encode_binary_vector,
|
|
decode_binary_vector,
|
|
melting_temperature,
|
|
qubo_energy,
|
|
gc_content,
|
|
raw_gc_content,
|
|
dna_to_greek,
|
|
encode_bytes_to_dna,
|
|
decode_dna_to_bytes,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# §1 QUBO-DNA CANDIDATE
|
|
# ============================================================
|
|
|
|
@dataclass
|
|
class QuboDnaCandidate:
|
|
"""A QUBO solution encoded as a DNA sequence with Tm metadata."""
|
|
x: List[int]
|
|
sequence: str
|
|
energy: float
|
|
tm: float
|
|
gc: float
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"x": self.x,
|
|
"sequence": self.sequence,
|
|
"energy": round(self.energy, 6),
|
|
"tm": round(self.tm, 4),
|
|
"gc": round(self.gc, 4),
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# §2 ENCODING
|
|
# ============================================================
|
|
|
|
def encode_qubo_candidate(
|
|
x: List[int],
|
|
Q: List[List[float]],
|
|
bits_per_var: int = 3,
|
|
) -> QuboDnaCandidate:
|
|
"""Encode a QUBO solution as a DNA sequence.
|
|
|
|
Uses the simple encoding: 0 → A (low Tm), 1 → P (high Tm).
|
|
Tm is monotonically proportional to the number of 1s in x.
|
|
|
|
Args:
|
|
x: binary solution vector
|
|
Q: QUBO matrix (for energy computation)
|
|
bits_per_var: bases per variable (default 3)
|
|
|
|
Returns:
|
|
QuboDnaCandidate with all metadata
|
|
"""
|
|
sequence = encode_binary_vector(x, bits_per_var)
|
|
energy = qubo_energy(x, Q)
|
|
tm = melting_temperature(sequence)
|
|
gc = raw_gc_content(sequence)
|
|
|
|
return QuboDnaCandidate(
|
|
x=x,
|
|
sequence=sequence,
|
|
energy=energy,
|
|
tm=tm,
|
|
gc=gc,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# §3 CANDIDATE GENERATION
|
|
# ============================================================
|
|
|
|
def generate_all_candidates(
|
|
Q: List[List[float]],
|
|
n_vars: int,
|
|
bits_per_var: int = 3,
|
|
) -> List[QuboDnaCandidate]:
|
|
"""Generate all QUBO candidates by brute force.
|
|
|
|
Args:
|
|
Q: QUBO matrix
|
|
n_vars: number of variables (must be <= 12 for brute force)
|
|
bits_per_var: bases per variable
|
|
|
|
Returns:
|
|
List of all QuboDnaCandidate (2^n_vars items)
|
|
"""
|
|
candidates = []
|
|
for i in range(2**n_vars):
|
|
x = [(i >> j) & 1 for j in range(n_vars)]
|
|
candidates.append(encode_qubo_candidate(x, Q, bits_per_var))
|
|
return candidates
|
|
|
|
|
|
def generate_random_candidates(
|
|
Q: List[List[float]],
|
|
n_vars: int,
|
|
n_samples: int = 1000,
|
|
seed: int = 42,
|
|
bits_per_var: int = 3,
|
|
) -> List[QuboDnaCandidate]:
|
|
"""Generate random QUBO candidates.
|
|
|
|
Args:
|
|
Q: QUBO matrix
|
|
n_vars: number of variables
|
|
n_samples: number of random samples
|
|
seed: RNG seed
|
|
bits_per_var: bases per variable
|
|
|
|
Returns:
|
|
List of QuboDnaCandidate
|
|
"""
|
|
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_qubo_candidate(list(x), Q, bits_per_var))
|
|
return candidates
|
|
|
|
|
|
# ============================================================
|
|
# §4 SORTING
|
|
# ============================================================
|
|
|
|
def sort_by_energy(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
|
|
"""Sort candidates by QUBO energy (ascending). Ground truth."""
|
|
return sorted(candidates, key=lambda c: c.energy)
|
|
|
|
|
|
def sort_by_tm(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
|
|
"""Sort candidates by melting temperature (ascending)."""
|
|
return sorted(candidates, key=lambda c: c.tm)
|
|
|
|
|
|
def sort_by_gc(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
|
|
"""Sort candidates by GC content (ascending)."""
|
|
return sorted(candidates, key=lambda c: c.gc)
|
|
|
|
|
|
def spearman_rank_correlation(values_a: List[float], values_b: List[float]) -> float:
|
|
"""Compute Spearman rank correlation.
|
|
|
|
Returns 1.0 for perfect positive correlation, -1.0 for perfect negative.
|
|
"""
|
|
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
|
|
|
|
|
|
# ============================================================
|
|
# §5 Tm SORT VERIFICATION
|
|
# ============================================================
|
|
|
|
@dataclass
|
|
class TmVerification:
|
|
"""Results of 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: QuboDnaCandidate
|
|
min_tm: QuboDnaCandidate
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"n_candidates": self.n_candidates,
|
|
"n_vars": self.n_vars,
|
|
"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 verify_tm_sort(
|
|
Q: List[List[float]],
|
|
n_vars: int,
|
|
n_samples: int = 0,
|
|
seed: int = 42,
|
|
top_k: int = 5,
|
|
bits_per_var: int = 3,
|
|
) -> TmVerification:
|
|
"""Verify that 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
|
|
bits_per_var: bases per variable
|
|
|
|
Returns:
|
|
TmVerification with results
|
|
"""
|
|
if n_samples == 0 and n_vars <= 12:
|
|
candidates = generate_all_candidates(Q, n_vars, bits_per_var)
|
|
else:
|
|
n_samples = n_samples or 10000
|
|
candidates = generate_random_candidates(Q, n_vars, n_samples, seed, bits_per_var)
|
|
|
|
energy_sorted = sort_by_energy(candidates)
|
|
tm_sorted = sort_by_tm(candidates)
|
|
|
|
energies = [c.energy for c in candidates]
|
|
tms = [c.tm for c in candidates]
|
|
corr = spearman_rank_correlation(energies, tms)
|
|
|
|
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 TmVerification(
|
|
n_candidates=len(candidates),
|
|
n_vars=n_vars,
|
|
tm_sort_matches_energy=(energy_sorted[0].x == tm_sorted[0].x),
|
|
rank_correlation=corr,
|
|
top_k_agreement=agreement,
|
|
top_k=top_k,
|
|
min_energy=energy_sorted[0],
|
|
min_tm=tm_sorted[0],
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# §6 SAM / FASTA EXPORT
|
|
# ============================================================
|
|
|
|
def candidates_to_sam(
|
|
candidates: List[QuboDnaCandidate],
|
|
name: str = "qubo_sort",
|
|
) -> str:
|
|
"""Export candidates to SAM format.
|
|
|
|
SAM is a tab-delimited format used in bioinformatics.
|
|
We use optional tags for energy, Tm, and solution vector.
|
|
|
|
Args:
|
|
candidates: list of QuboDnaCandidate
|
|
name: sample name
|
|
|
|
Returns:
|
|
SAM-formatted string
|
|
"""
|
|
lines = [
|
|
"@HD\tVN:1.6\tSO:unsorted",
|
|
f"@PG\tID:{name}\tPN:{name}\tVN:0.1",
|
|
]
|
|
for i, cand in enumerate(candidates):
|
|
tags = [
|
|
f"TM:f:{cand.tm:.4f}",
|
|
f"EN:f:{cand.energy:.6f}",
|
|
f"GC:f:{cand.gc:.4f}",
|
|
f"XV:Z:{','.join(str(v) for v in cand.x)}",
|
|
]
|
|
line = (
|
|
f"cand_{i:06d}\t0\t*\t0\t0\t*\t*\t0\t{len(cand.sequence)}\t"
|
|
f"{cand.sequence}\t{'~' * len(cand.sequence)}\t"
|
|
+ "\t".join(tags)
|
|
)
|
|
lines.append(line)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def candidates_to_fasta(
|
|
candidates: List[QuboDnaCandidate],
|
|
prefix: str = "qubo",
|
|
) -> str:
|
|
"""Export candidates to FASTA format.
|
|
|
|
FASTA is a simple format with header lines (>) followed by sequence.
|
|
|
|
Args:
|
|
candidates: list of QuboDnaCandidate
|
|
prefix: prefix for sequence IDs
|
|
|
|
Returns:
|
|
FASTA-formatted string
|
|
"""
|
|
lines = []
|
|
for i, cand in enumerate(candidates):
|
|
header = f">{prefix}_cand_{i:06d} energy={cand.energy:.6f} tm={cand.tm:.4f}"
|
|
lines.append(header)
|
|
lines.append(cand.sequence)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
# ============================================================
|
|
# §7 DEMO QUBOs
|
|
# ============================================================
|
|
|
|
def demo_max_cut(n: int = 6, seed: int = 42) -> List[List[float]]:
|
|
"""Generate a Max-Cut QUBO.
|
|
|
|
Max-Cut: E(x) = Σ_{(i,j) in E} (x_i + x_j - 2*x_i*x_j)
|
|
Equivalent to: Q_ii = degree(i), Q_ij = -2 for edges
|
|
"""
|
|
rng = random.Random(seed)
|
|
Q = [[0.0] * n for _ in range(n)]
|
|
|
|
# Create a random graph (Erdos-Renyi with p=0.5)
|
|
for i in range(n):
|
|
for j in range(i + 1, n):
|
|
if rng.random() < 0.5:
|
|
Q[i][i] += 1.0
|
|
Q[j][j] += 1.0
|
|
Q[i][j] = -2.0
|
|
Q[j][i] = -2.0
|
|
|
|
return Q
|
|
|
|
|
|
def demo_number_partition(seed: int = 42) -> List[List[float]]:
|
|
"""Generate a small Number Partitioning QUBO.
|
|
|
|
NPP: partition numbers into two sets with equal sum.
|
|
Q_ii = a_i^2, Q_ij = 2*a_i*a_j
|
|
"""
|
|
rng = random.Random(seed)
|
|
numbers = [rng.randint(1, 20) for _ in range(6)]
|
|
n = len(numbers)
|
|
Q = [[0.0] * n for _ in range(n)]
|
|
|
|
for i in range(n):
|
|
Q[i][i] = numbers[i] ** 2
|
|
for j in range(i + 1, n):
|
|
Q[i][j] = 2.0 * numbers[i] * numbers[j]
|
|
Q[j][i] = 2.0 * numbers[i] * numbers[j]
|
|
|
|
return Q
|
|
|
|
|
|
# ============================================================
|
|
# §8 FULL PIPELINE
|
|
# ============================================================
|
|
|
|
def run_qubo_dna_sort(
|
|
Q: List[List[float]],
|
|
n_vars: int,
|
|
n_samples: int = 0,
|
|
seed: int = 42,
|
|
bits_per_var: int = 3,
|
|
) -> dict:
|
|
"""Run the full QUBO-DNA sort pipeline.
|
|
|
|
Args:
|
|
Q: QUBO matrix
|
|
n_vars: number of variables
|
|
n_samples: 0 for brute force, else sampling
|
|
seed: RNG seed
|
|
bits_per_var: bases per variable
|
|
|
|
Returns:
|
|
Summary dict with verification results
|
|
"""
|
|
print(f"QUBO-DNA Sort Pipeline")
|
|
print(f" Variables: {n_vars}")
|
|
|
|
if n_samples == 0 and n_vars <= 12:
|
|
candidates = generate_all_candidates(Q, n_vars, bits_per_var)
|
|
print(f" Brute force: {len(candidates)} candidates")
|
|
else:
|
|
n_samples = n_samples or 10000
|
|
candidates = generate_random_candidates(Q, n_vars, n_samples, seed, bits_per_var)
|
|
print(f" Random sampling: {len(candidates)} candidates")
|
|
|
|
# Sort
|
|
energy_sorted = sort_by_energy(candidates)
|
|
tm_sorted = sort_by_tm(candidates)
|
|
|
|
print(f"\n Min energy: {energy_sorted[0].energy:.4f}, x={energy_sorted[0].x}")
|
|
print(f" Min Tm: {tm_sorted[0].tm:.4f}, x={tm_sorted[0].x}")
|
|
|
|
# Verify
|
|
verification = verify_tm_sort(Q, n_vars, n_samples, seed, bits_per_var)
|
|
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}")
|
|
|
|
return {
|
|
"n_vars": n_vars,
|
|
"n_candidates": len(candidates),
|
|
"energy_optimal": energy_sorted[0].to_dict(),
|
|
"tm_optimal": tm_sorted[0].to_dict(),
|
|
"verification": verification.to_dict(),
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# §9 CLI
|
|
# ============================================================
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print("QUBO-DNA Sort: Naive Tm Encoding")
|
|
print("=" * 60)
|
|
|
|
# Demo 1: Diagonal QUBO (ideal case)
|
|
print("\n--- Demo 1: Diagonal QUBO (6 vars) ---")
|
|
n = 6
|
|
Q = [[3.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
|
|
r1 = run_qubo_dna_sort(Q, n)
|
|
|
|
# Demo 2: Max-Cut
|
|
print("\n--- Demo 2: Max-Cut (6 vars) ---")
|
|
Q2 = demo_max_cut(6, seed=42)
|
|
r2 = run_qubo_dna_sort(Q2, 6)
|
|
|
|
# Demo 3: Ising (negative diagonal)
|
|
print("\n--- Demo 3: Ising Chain (6 vars) ---")
|
|
Q3 = [[-1.0 if i == j else (-0.5 if abs(i-j) == 1 else 0.0) for j in range(6)] for i in range(6)]
|
|
r3 = run_qubo_dna_sort(Q3, 6)
|
|
|
|
print("\n" + "=" * 60)
|
|
print("DONE")
|
|
print("=" * 60)
|