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
BIN
.openclaw/tmp/surface/hachimoji_surface.bmp
Normal file
|
After Width: | Height: | Size: 246 B |
4
.openclaw/tmp/surface/hachimoji_surface.ppm
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
P6
|
||||||
|
8 8
|
||||||
|
255
|
||||||
|
|
||||||
BIN
.openclaw/tmp/surface/hachimoji_surface_32x32.bmp
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
.openclaw/tmp/surface/ising_mid.bmp
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
.openclaw/tmp/surface/ising_mid_64x64.bmp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
.openclaw/tmp/surface/ising_optimal.bmp
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
.openclaw/tmp/surface/ising_optimal_64x64.bmp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
.openclaw/tmp/surface/ising_worst.bmp
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
.openclaw/tmp/surface/ising_worst_64x64.bmp
Normal file
|
After Width: | Height: | Size: 12 KiB |
580
python/dna_qubo_sort.py
Normal file
|
|
@ -0,0 +1,580 @@
|
||||||
|
#!/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)
|
||||||
96
python/dna_webgpu.html
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>DNA Braid Sort: WebGPU QUBO Solver</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 2em; }
|
||||||
|
h1 { color: #0ff; }
|
||||||
|
#output { white-space: pre; line-height: 1.4; }
|
||||||
|
.optimal { color: #ff0; }
|
||||||
|
.error { color: #f00; }
|
||||||
|
.info { color: #888; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🧬 DNA Braid Sort — WebGPU QUBO Solver</h1>
|
||||||
|
<p class="info">Treats GPU actions as triangle math. Braid crossings = compare-swap. Eigensolid = sorted output.</p>
|
||||||
|
<div id="output">Initializing WebGPU...</div>
|
||||||
|
|
||||||
|
<script src="dna_webgpu.js"></script>
|
||||||
|
<script>
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
|
||||||
|
function log(msg, cls = '') {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = cls;
|
||||||
|
span.textContent = msg + '\n';
|
||||||
|
output.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
if (!navigator.gpu) {
|
||||||
|
log('ERROR: WebGPU not supported in this browser.', 'error');
|
||||||
|
log('Try Chrome 113+ or Edge 113+ with --enable-unsafe-webgpu flag.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('='.repeat(60));
|
||||||
|
log('DNA Braid Sort: WebGPU QUBO Solver');
|
||||||
|
log('='.repeat(60));
|
||||||
|
|
||||||
|
const solver = new DNABraidSolver();
|
||||||
|
await solver.init();
|
||||||
|
log('✓ WebGPU initialized');
|
||||||
|
|
||||||
|
// Run demos
|
||||||
|
for (const nVars of [10, 12, 14]) {
|
||||||
|
const Q = generateBandedQUBO(nVars, 42);
|
||||||
|
const n = 1 << nVars;
|
||||||
|
|
||||||
|
log(`\n--- ${nVars} variables, ${n.toLocaleString()} solutions ---`);
|
||||||
|
|
||||||
|
const result = await solver.solveQUBO(Q, nVars);
|
||||||
|
|
||||||
|
log(`Optimal: x=[${result.solution.slice(0, 8)}...], E=${result.energy.toFixed(4)}`, 'optimal');
|
||||||
|
log(`Encode: ${result.encodeTime.toFixed(1)}ms`);
|
||||||
|
log(`Sort: ${result.sortTime.toFixed(1)}ms`);
|
||||||
|
log(`Total: ${result.totalTime.toFixed(1)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log('\n' + '='.repeat(60));
|
||||||
|
log('DONE');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log(`ERROR: ${e.message}`, 'error');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBandedQUBO(n, seed) {
|
||||||
|
const rng = mulberry32(seed);
|
||||||
|
const Q = Array.from({ length: n }, () => Array(n).fill(0));
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
Q[i][i] = 2 + rng() * 6;
|
||||||
|
if (i + 1 < n) {
|
||||||
|
const c = -(0.5 + rng() * 2.5);
|
||||||
|
Q[i][i + 1] = c;
|
||||||
|
Q[i + 1][i] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Q;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mulberry32(seed) {
|
||||||
|
return function() {
|
||||||
|
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
|
||||||
|
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
||||||
|
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
||||||
|
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||