SilverSight/python/dna_qubo_sort.py
allaunthefox 7327775e16 feat(dna): add remaining source files and surface images
- python/dna_qubo_sort.py — QUBO-DNA sort with SAM/FASTA export
- python/dna_webgpu.html — WebGPU browser demo
- .openclaw/tmp/surface/*.bmp — rendered surface images
2026-06-23 02:27:40 +00:00

580 lines
18 KiB
Python
Raw 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_sort.py — QUBO Energy Minimization via DNA Sorting
The "backdoor energy sort": encode QUBO candidate solutions as Hachimoji
DNA sequences where melting temperature (Tm) correlates with QUBO energy.
Then sort by Tm — the sorted order IS the energy ranking.
Theory:
QUBO energy E(x) = x^T Q x
For a diagonal-dominant QUBO with non-negative entries:
E(x) ≈ Σ_i Q_ii · x_i + cross terms
If we encode x_i=0 → A/T (low Tm) and x_i=1 → G/C (high Tm),
then Tm(sequence) ∝ Σ_i (Tm contribution for x_i) ∝ E(x).
Sorting by Tm (ascending) = sorting by energy (ascending).
Top of sorted list = minimum energy = optimal solution.
Adleman's insight (1994): let molecular physics do the sorting.
- Generate all 2^n candidate sequences (massively parallel)
- Sort by physical property (gel electrophoresis, affinity)
- Read the result
Modern insight: samtools sort processes billions of reads.
The infrastructure already exists.
"""
from __future__ import annotations
import hashlib
import json
import math
import random
import struct
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from dna_codec import (
HACHIMOJI_BASES,
decode_binary_vector,
encode_binary_vector,
gc_content,
melting_temperature,
qubo_energy,
sequence_stats,
)
from q16_canonical import float_to_q16, q16_to_float, q16_to_bytes, q16_from_bytes
# ============================================================
# §1 QUBO → DNA ENCODING
# ============================================================
@dataclass
class QuboDnaCandidate:
"""A single QUBO solution encoded as a DNA sequence."""
x: List[int] # binary solution vector
sequence: str # Hachimoji DNA encoding
energy: float # QUBO energy E(x)
energy_q16: int # Q16_16 fixed-point energy
tm: float # melting temperature
gc: float # GC-equivalent content
def to_dict(self) -> dict:
return {
"x": self.x,
"sequence": self.sequence,
"energy": round(self.energy, 6),
"energy_q16": self.energy_q16,
"tm": round(self.tm, 4),
"gc": round(self.gc, 4),
}
def encode_qubo_candidate(
x: List[int],
Q: List[List[float]],
bits_per_var: int = 3,
) -> QuboDnaCandidate:
"""Encode a single QUBO solution as a DNA candidate.
The encoding maps:
x_i = 0 → 'A' repeated bits_per_var times (low Tm)
x_i = 1 → 'P' repeated bits_per_var times (high Tm)
This ensures Tm(sequence) is monotonically related to
the number of 1s in x, which drives energy for non-negative Q.
Args:
x: binary solution vector
Q: QUBO matrix
bits_per_var: bases per variable
Returns:
QuboDnaCandidate with all metadata
"""
sequence = encode_binary_vector(x, bits_per_var)
energy = qubo_energy(x, Q)
energy_q16 = float_to_q16(energy)
tm = melting_temperature(sequence)
gc = gc_content(sequence)
return QuboDnaCandidate(
x=x,
sequence=sequence,
energy=energy,
energy_q16=energy_q16,
tm=tm,
gc=gc,
)
# ============================================================
# §2 BRUTE-FORCE GENERATION (small QUBOs)
# ============================================================
def generate_all_candidates(
Q: List[List[float]],
n_vars: int,
bits_per_var: int = 3,
max_candidates: int = 4096,
) -> List[QuboDnaCandidate]:
"""Generate all 2^n candidate solutions and encode as DNA.
Only feasible for small n (n ≤ 12 gives 4096 candidates).
For larger QUBOs, use generate_random_candidates().
Args:
Q: QUBO matrix
n_vars: number of binary variables
bits_per_var: bases per variable
max_candidates: safety cap
Returns:
List of QuboDnaCandidate, one per solution
"""
if n_vars > 12:
raise ValueError(
f"n_vars={n_vars} too large for brute force (2^{n_vars} = {2**n_vars}). "
f"Use generate_random_candidates() instead."
)
candidates = []
for i in range(min(2**n_vars, max_candidates)):
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 = 10000,
seed: int = 42,
bits_per_var: int = 3,
) -> List[QuboDnaCandidate]:
"""Generate random candidate solutions for large QUBOs.
Args:
Q: QUBO matrix
n_vars: number of binary variables
n_samples: number of random samples
seed: RNG seed for reproducibility
bits_per_var: bases per variable
Returns:
List of QuboDnaCandidate
"""
rng = random.Random(seed)
candidates = []
seen = set()
for _ in range(n_samples):
# Generate random binary vector
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
# ============================================================
# §3 DNA SORTING (the backdoor)
# ============================================================
def sort_by_tm(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
"""Sort candidates by melting temperature (ascending).
Low Tm = low GC content = fewer 1s in x = lower energy (for non-negative Q).
This is the "backdoor" — sorting by a physical property IS energy ranking.
Args:
candidates: list of QuboDnaCandidate
Returns:
Sorted list (lowest Tm first = lowest energy first)
"""
return sorted(candidates, key=lambda c: c.tm)
def sort_by_gc(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
"""Sort candidates by GC-equivalent content (ascending)."""
return sorted(candidates, key=lambda c: c.gc)
def sort_by_energy(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
"""Sort candidates by actual QUBO energy (ascending). Ground truth."""
return sorted(candidates, key=lambda c: c.energy)
def sort_by_energy_q16(candidates: List[QuboDnaCandidate]) -> List[QuboDnaCandidate]:
"""Sort candidates by Q16_16 fixed-point energy (ascending)."""
return sorted(candidates, key=lambda c: c.energy_q16)
# ============================================================
# §4 SAMTOOLS COMPATIBLE OUTPUT
# ============================================================
def candidates_to_sam(
candidates: List[QuboDnaCandidate],
qubo_name: str = "QUBO",
) -> str:
"""Export candidates as a SAM file for samtools sort.
Each candidate becomes a SAM read with:
- QNAME: energy ranking
- SEQ: Hachimoji DNA sequence
- QUAL: quality scores based on Tm
- TAG: energy, gc content
This produces a valid SAM file that can be sorted with:
samtools sort -t TM -o sorted.bam input.sam
Args:
candidates: list of QuboDnaCandidate
qubo_name: name for the QUBO problem
Returns:
SAM format string
"""
lines = []
# SAM header
lines.append("@HD\tVN:1.6\tSO:unsorted")
lines.append(f"@PG\tID:dna_qubo_sort\tPN:dna_qubo_sort\tVN:0.1")
lines.append(f"@CO\tQUBO problem: {qubo_name}, {len(candidates)} candidates")
for i, cand in enumerate(candidates):
qname = f"{qubo_name}_{i:06d}"
flag = 0
rname = "*"
pos = 0
mapq = 0
cigar = "*"
rnext = "*"
pnext = 0
tlen = len(cand.sequence)
seq = cand.sequence
qual = "~" * len(seq) # placeholder quality
# Custom tags
tags = [
f"TM:f:{cand.tm:.4f}",
f"GC:f:{cand.gc:.4f}",
f"EN:f:{cand.energy:.6f}",
f"EQ:i:{cand.energy_q16}",
f"XV:Z:{','.join(str(v) for v in cand.x)}",
]
line = "\t".join(
str(x) for x in [qname, flag, rname, pos, mapq, cigar, rnext, pnext, tlen, seq, qual]
) + "\t" + "\t".join(tags)
lines.append(line)
return "\n".join(lines) + "\n"
def candidates_to_fasta(candidates: List[QuboDnaCandidate]) -> str:
"""Export candidates as FASTA (sequence only, for external tools)."""
lines = []
for i, cand in enumerate(candidates):
lines.append(f">candidate_{i:06d} energy={cand.energy:.6f} tm={cand.tm:.4f}")
# Wrap at 80 chars
seq = cand.sequence
for j in range(0, len(seq), 80):
lines.append(seq[j : j + 80])
return "\n".join(lines) + "\n"
# ============================================================
# §5 VERIFICATION: does Tm sorting = energy sorting?
# ============================================================
@dataclass
class SortVerification:
"""Results of verifying that Tm sort matches energy sort."""
n_candidates: int
n_vars: int
tm_sort_matches_energy: bool
rank_correlation: float # Spearman-like correlation
top_k_agreement: int # number of top-k that match
top_k: int
min_energy_candidate: QuboDnaCandidate
min_tm_candidate: QuboDnaCandidate
energy_sort: List[QuboDnaCandidate]
tm_sort: List[QuboDnaCandidate]
def to_dict(self) -> dict:
return {
"n_candidates": self.n_candidates,
"n_vars": self.n_vars,
"tm_sort_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_candidate.to_dict(),
"min_tm": self.min_tm_candidate.to_dict(),
}
def spearman_rank_correlation(
values_a: List[float],
values_b: List[float],
) -> float:
"""Compute Spearman rank correlation between two value lists."""
n = len(values_a)
if n < 2:
return 0.0
def rank(vals):
sorted_vals = sorted(range(n), key=lambda i: vals[i])
ranks = [0] * n
for rank_val, idx in enumerate(sorted_vals):
ranks[idx] = rank_val
return ranks
rank_a = rank(values_a)
rank_b = rank(values_b)
# Spearman formula: 1 - (6 * Σd²) / (n * (n² - 1))
d_sq_sum = sum((ra - rb) ** 2 for ra, rb in zip(rank_a, rank_b))
denom = n * (n * n - 1)
if denom == 0:
return 0.0
return 1.0 - (6.0 * d_sq_sum) / denom
def verify_tm_sort(
Q: List[List[float]],
n_vars: int,
n_samples: int = 0,
seed: int = 42,
top_k: int = 5,
) -> SortVerification:
"""Verify that sorting by Tm gives the same order as sorting by energy.
This is THE critical test. If Tm sort ≠ energy sort, the backdoor doesn't work.
Args:
Q: QUBO matrix
n_vars: number of variables
n_samples: 0 for brute force, >0 for random sampling
seed: RNG seed
top_k: number of top candidates to compare
Returns:
SortVerification with detailed results
"""
if n_samples == 0:
candidates = generate_all_candidates(Q, n_vars)
else:
candidates = generate_random_candidates(Q, n_vars, n_samples, seed)
energy_sorted = sort_by_energy(candidates)
tm_sorted = sort_by_tm(candidates)
# Rank correlation
energies = [c.energy for c in candidates]
tms = [c.tm for c in candidates]
corr = spearman_rank_correlation(energies, tms)
# Top-k agreement
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 SortVerification(
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_candidate=energy_sorted[0],
min_tm_candidate=tm_sorted[0],
energy_sort=energy_sorted,
tm_sort=tm_sorted,
)
# ============================================================
# §6 DEMO QUBO PROBLEMS
# ============================================================
def demo_max_cut(n: int = 6, seed: int = 42) -> List[List[float]]:
"""Generate a random Max-Cut QUBO.
Max-Cut: partition vertices into two sets to maximize edges between them.
QUBO formulation: minimize -Σ_{(i,j)∈E} x_i(1-x_j) + x_j(1-x_i)
Args:
n: number of vertices
seed: RNG seed
Returns:
QUBO matrix (n×n)
"""
rng = random.Random(seed)
Q = [[0.0] * n for _ in range(n)]
# Random graph with ~50% edge probability
for i in range(n):
for j in range(i + 1, n):
if rng.random() < 0.5:
# Edge exists: reward for having endpoints in different sets
Q[i][j] = -1.0
Q[j][i] = -1.0
Q[i][i] += 1.0
Q[j][j] += 1.0
return Q
def demo_number_partition(numbers: List[int]) -> List[List[float]]:
"""Generate a Number Partitioning QUBO.
Given numbers, partition into two sets with equal sum.
QUBO: minimize (Σ s_i · a_i)² where s_i ∈ {-1, +1}
Args:
numbers: list of integers to partition
Returns:
QUBO matrix
"""
n = len(numbers)
Q = [[0.0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
Q[i][j] = float(numbers[i] * numbers[j])
return Q
# ============================================================
# §7 FULL PIPELINE
# ============================================================
def run_qubo_dna_sort(
Q: List[List[float]],
n_vars: int,
n_samples: int = 0,
seed: int = 42,
output_prefix: str = "qubo_dna",
) -> dict:
"""Run the full QUBO → DNA → Sort → Verify pipeline.
Args:
Q: QUBO matrix
n_vars: number of variables
n_samples: 0 for brute force, >0 for sampling
seed: RNG seed
output_prefix: prefix for output files
Returns:
Summary dict
"""
print(f"QUBO-DNA Sort Pipeline")
print(f" Variables: {n_vars}")
print(f" Matrix size: {len(Q)}×{len(Q[0])}")
# Generate candidates
if n_samples == 0 and n_vars <= 12:
candidates = generate_all_candidates(Q, n_vars)
print(f" Generated: {len(candidates)} (brute force)")
else:
n_samples = n_samples or 10000
candidates = generate_random_candidates(Q, n_vars, n_samples, seed)
print(f" Generated: {len(candidates)} (random sampling)")
# Sort by different methods
energy_sorted = sort_by_energy(candidates)
tm_sorted = sort_by_tm(candidates)
gc_sorted = sort_by_gc(candidates)
print(f"\n Energy-optimal: x={energy_sorted[0].x}, E={energy_sorted[0].energy:.6f}")
print(f" Tm-optimal: x={tm_sorted[0].x}, E={tm_sorted[0].energy:.6f}")
print(f" GC-optimal: x={gc_sorted[0].x}, E={gc_sorted[0].energy:.6f}")
# Verify Tm sort
verification = verify_tm_sort(Q, n_vars, n_samples if n_samples else 0, 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}")
# Export SAM
sam_content = candidates_to_sam(candidates, output_prefix)
sam_path = f"{output_prefix}.sam"
with open(sam_path, "w") as f:
f.write(sam_content)
print(f"\n SAM output: {sam_path} ({len(candidates)} reads)")
# Export FASTA
fasta_content = candidates_to_fasta(candidates)
fasta_path = f"{output_prefix}.fasta"
with open(fasta_path, "w") as f:
f.write(fasta_content)
print(f" FASTA output: {fasta_path}")
# Summary
summary = {
"n_vars": n_vars,
"n_candidates": len(candidates),
"energy_optimal": energy_sorted[0].to_dict(),
"tm_optimal": tm_sorted[0].to_dict(),
"gc_optimal": gc_sorted[0].to_dict(),
"verification": verification.to_dict(),
"files": {
"sam": sam_path,
"fasta": fasta_path,
},
}
# Write summary JSON
json_path = f"{output_prefix}_summary.json"
with open(json_path, "w") as f:
json.dump(summary, f, indent=2)
print(f" Summary: {json_path}")
return summary
# ============================================================
# §8 CLI
# ============================================================
if __name__ == "__main__":
import sys
print("=" * 60)
print("QUBO-DNA Sort: Backdoor Energy Minimization")
print("=" * 60)
# Demo 1: Small Max-Cut
print("\n--- Demo 1: Max-Cut (6 vertices, brute force) ---")
Q1 = demo_max_cut(6, seed=42)
result1 = run_qubo_dna_sort(Q1, n_vars=6, output_prefix="qubo_dna_maxcut6")
# Demo 2: Number Partition
print("\n--- Demo 2: Number Partition ---")
numbers = [3, 7, 1, 5, 9, 2, 8, 4]
Q2 = demo_number_partition(numbers)
result2 = run_qubo_dna_sort(Q2, n_vars=len(numbers), output_prefix="qubo_dna_partition8")
# Demo 3: Larger random QUBO
print("\n--- Demo 3: Random QUBO (10 vars, sampling) ---")
rng = random.Random(42)
n3 = 10
Q3 = [[rng.uniform(-2, 2) if i != j else rng.uniform(0, 5) for j in range(n3)] for i in range(n3)]
result3 = run_qubo_dna_sort(Q3, n_vars=n3, n_samples=5000, output_prefix="qubo_dna_random10")
print("\n" + "=" * 60)
print("DONE")
print("=" * 60)