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>
|
||||