From 7327775e16396c4c941191db0298644cb7149fce Mon Sep 17 00:00:00 2001 From: allaunthefox Date: Tue, 23 Jun 2026 02:27:40 +0000 Subject: [PATCH] feat(dna): add remaining source files and surface images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .openclaw/tmp/surface/hachimoji_surface.bmp | Bin 0 -> 246 bytes .openclaw/tmp/surface/hachimoji_surface.ppm | 4 + .../tmp/surface/hachimoji_surface_32x32.bmp | Bin 0 -> 3126 bytes .openclaw/tmp/surface/ising_mid.bmp | Bin 0 -> 246 bytes .openclaw/tmp/surface/ising_mid_64x64.bmp | Bin 0 -> 12342 bytes .openclaw/tmp/surface/ising_optimal.bmp | Bin 0 -> 246 bytes .openclaw/tmp/surface/ising_optimal_64x64.bmp | Bin 0 -> 12342 bytes .openclaw/tmp/surface/ising_worst.bmp | Bin 0 -> 246 bytes .openclaw/tmp/surface/ising_worst_64x64.bmp | Bin 0 -> 12342 bytes python/dna_qubo_sort.py | 580 ++++++++++++++++++ python/dna_webgpu.html | 96 +++ 11 files changed, 680 insertions(+) create mode 100644 .openclaw/tmp/surface/hachimoji_surface.bmp create mode 100644 .openclaw/tmp/surface/hachimoji_surface.ppm create mode 100644 .openclaw/tmp/surface/hachimoji_surface_32x32.bmp create mode 100644 .openclaw/tmp/surface/ising_mid.bmp create mode 100644 .openclaw/tmp/surface/ising_mid_64x64.bmp create mode 100644 .openclaw/tmp/surface/ising_optimal.bmp create mode 100644 .openclaw/tmp/surface/ising_optimal_64x64.bmp create mode 100644 .openclaw/tmp/surface/ising_worst.bmp create mode 100644 .openclaw/tmp/surface/ising_worst_64x64.bmp create mode 100644 python/dna_qubo_sort.py create mode 100644 python/dna_webgpu.html diff --git a/.openclaw/tmp/surface/hachimoji_surface.bmp b/.openclaw/tmp/surface/hachimoji_surface.bmp new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f87d8fc8bee56471683c8a6ae77336172b8 GIT binary patch literal 246 mcmZ?r{l)+RW0Fq`6)c^nh literal 0 HcmV?d00001 diff --git a/.openclaw/tmp/surface/ising_mid_64x64.bmp b/.openclaw/tmp/surface/ising_mid_64x64.bmp new file mode 100644 index 0000000000000000000000000000000000000000..b047575898a8b73ea85a5ef902a48c93000e2378 GIT binary patch literal 12342 zcmeIuF$#b%3mB@r^tGzNkhD% z;=gZmawDZq5g z0{iDa^_dMbHGD0XKMU~B0$Hy=&DZ@80RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 J2oU(azyr9 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) diff --git a/python/dna_webgpu.html b/python/dna_webgpu.html new file mode 100644 index 00000000..578a59fc --- /dev/null +++ b/python/dna_webgpu.html @@ -0,0 +1,96 @@ + + + + DNA Braid Sort: WebGPU QUBO Solver + + + +

🧬 DNA Braid Sort — WebGPU QUBO Solver

+

Treats GPU actions as triangle math. Braid crossings = compare-swap. Eigensolid = sorted output.

+
Initializing WebGPU...
+ + + + +