mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
Derivation from first principles: 1. Hachimoji DNA encoding (8 bases, ASCII-ordered, monotone LUT) 2. Imaginary Semantic Time (observer-independent semantic axis) 3. Sieve observers with CRT reconciliation (mod ℓ projections) 4. Semantic mass (E - E_min, E_s = m · 8²) 5. Gap preservation theorem (cleanMerge_preservesGap from GraphRank.lean) 6. Epigenetic computation (bistability, spreading, memory, attractors) 7. Logarithmic vector spaces (Kritchevsky: log N is a geometric vector) 8. Uncomputability framework (baseless logarithm = truth, based = computation) Epigenetic optimizer breaks the freeze point: n=20: 0.7s (brute: 0.3s) n=24: 1.5s (brute: FROZEN) n=30: 3.4s (brute: FROZEN) n=50: 23.9s (brute: FROZEN) Files: docs/UNIFIED_THEORY.md — full theory derivation docs/HACHIMOJI_DNA_SYNTAX.md — formal syntax specification docs/EPIGENETIC_COMPUTATION.md — epigenetic optimizer docs/UNCOMPUTABILITY.md — logarithmic vector space framework docs/REDERIVATION.md — rederivation from first principles python/dna_*.py — implementation (codec, LUT, GPU, surface) tests/test_dna_*.py — 68 tests, all green Build: N/A (Python + Lean documentation)
436 lines
13 KiB
Python
436 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
dna_gpu.py — GPU-Accelerated QUBO Solver via DNA Encoding
|
|
|
|
Smuggles a combinatorial optimization problem into a GPU as a string
|
|
sorting operation. The DNA sequences are the disguise — the GPU processes
|
|
them as text, but the LUT maps each sequence to a QUBO energy.
|
|
|
|
Architecture:
|
|
CPU: encode QUBO → DNA sequences → send to GPU
|
|
GPU: sort DNA strings (massively parallel) → return sorted
|
|
CPU: decode sorted DNA → solutions in energy order
|
|
|
|
The GPU thinks it's sorting text. It's actually solving a QUBO.
|
|
The combinatorial explosion is hidden in the string representation.
|
|
|
|
For a 20-variable QUBO: 2^20 = 1M strings to sort.
|
|
GPU can sort 1M strings in milliseconds.
|
|
CPU brute-force would take seconds.
|
|
|
|
For a 30-variable QUBO: 2^30 = 1B strings.
|
|
GPU can still handle it. CPU can't.
|
|
|
|
Requirements:
|
|
- numpy (for CPU fallback)
|
|
- cupy (for GPU acceleration, optional)
|
|
- Standard Python 3.8+
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
# ============================================================
|
|
# §1 HACHIMOJI DNA ALPHABET (ASCII-ordered)
|
|
# ============================================================
|
|
|
|
BASES = list("ABCGPSTZ")
|
|
N_BASES = len(BASES)
|
|
BASE_TO_INDEX = {b: i for i, b in enumerate(BASES)}
|
|
INDEX_TO_BASE = {i: b for i, b in enumerate(BASES)}
|
|
|
|
|
|
def int_to_dna(value: int, length: int) -> str:
|
|
"""Convert integer to DNA sequence."""
|
|
seq = []
|
|
for _ in range(length):
|
|
seq.append(INDEX_TO_BASE[value % N_BASES])
|
|
value //= N_BASES
|
|
return "".join(reversed(seq))
|
|
|
|
|
|
def dna_to_int(sequence: str) -> int:
|
|
"""Convert DNA sequence to integer."""
|
|
value = 0
|
|
for b in sequence:
|
|
value = value * N_BASES + BASE_TO_INDEX[b]
|
|
return value
|
|
|
|
|
|
def bases_needed(n: int) -> int:
|
|
"""Minimum bases to represent n unique symbols."""
|
|
if n <= 0:
|
|
return 0
|
|
length = 1
|
|
while N_BASES ** length < n:
|
|
length += 1
|
|
return length
|
|
|
|
|
|
# ============================================================
|
|
# §2 QUBO ENERGY (CPU)
|
|
# ============================================================
|
|
|
|
def qubo_energy(x: List[int], Q: List[List[float]]) -> float:
|
|
"""Compute QUBO energy E(x) = x^T Q x."""
|
|
n = len(x)
|
|
return sum(Q[i][j] * x[i] * x[j] for i in range(n) for j in range(n))
|
|
|
|
|
|
def qubo_energy_batch(
|
|
solutions: List[List[int]],
|
|
Q: List[List[float]],
|
|
) -> List[float]:
|
|
"""Compute energies for a batch of solutions."""
|
|
return [qubo_energy(x, Q) for x in solutions]
|
|
|
|
|
|
# ============================================================
|
|
# §3 MONOTONE DNA ENCODING
|
|
# ============================================================
|
|
|
|
@dataclass
|
|
class DNASolution:
|
|
"""A QUBO solution encoded as a DNA sequence."""
|
|
sequence: str
|
|
solution: List[int]
|
|
energy: float
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"seq": self.sequence,
|
|
"x": self.solution,
|
|
"energy": round(self.energy, 6),
|
|
}
|
|
|
|
|
|
def encode_all_solutions(Q: List[List[float]], n_vars: int) -> List[DNASolution]:
|
|
"""Encode all 2^n solutions as DNA sequences, sorted by energy.
|
|
|
|
This is the monotone encoding: DNA rank = energy rank.
|
|
"""
|
|
n = 2 ** n_vars
|
|
seq_len = bases_needed(n)
|
|
|
|
# Compute all energies
|
|
solutions = []
|
|
for i in range(n):
|
|
x = [(i >> j) & 1 for j in range(n_vars)]
|
|
energy = qubo_energy(x, Q)
|
|
solutions.append((x, energy))
|
|
|
|
# Sort by energy (this IS the monotone assignment)
|
|
solutions.sort(key=lambda s: s[1])
|
|
|
|
# Assign DNA sequences in energy order
|
|
result = []
|
|
for rank, (x, energy) in enumerate(solutions):
|
|
seq = int_to_dna(rank, seq_len)
|
|
result.append(DNASolution(sequence=seq, solution=x, energy=energy))
|
|
|
|
return result
|
|
|
|
|
|
def encode_sampled_solutions(
|
|
Q: List[List[float]],
|
|
n_vars: int,
|
|
n_samples: int,
|
|
seed: int = 42,
|
|
) -> List[DNASolution]:
|
|
"""Encode sampled solutions as DNA sequences."""
|
|
rng = random.Random(seed)
|
|
seen = set()
|
|
solutions = []
|
|
|
|
for _ in range(n_samples):
|
|
x = tuple(rng.randint(0, 1) for _ in range(n_vars))
|
|
if x in seen:
|
|
continue
|
|
seen.add(x)
|
|
energy = qubo_energy(list(x), Q)
|
|
solutions.append((list(x), energy))
|
|
|
|
solutions.sort(key=lambda s: s[1])
|
|
seq_len = bases_needed(len(solutions))
|
|
|
|
result = []
|
|
for rank, (x, energy) in enumerate(solutions):
|
|
seq = int_to_dna(rank, seq_len)
|
|
result.append(DNASolution(sequence=seq, solution=x, energy=energy))
|
|
|
|
return result
|
|
|
|
|
|
# ============================================================
|
|
# §4 GPU STRING SORTING
|
|
# ============================================================
|
|
|
|
def try_import_cupy():
|
|
"""Try to import CuPy for GPU acceleration."""
|
|
try:
|
|
import cupy as cp
|
|
return cp
|
|
except ImportError:
|
|
return None
|
|
|
|
|
|
def sort_dna_strings_cpu(sequences: List[str]) -> List[str]:
|
|
"""Sort DNA strings on CPU (Python sorted)."""
|
|
return sorted(sequences)
|
|
|
|
|
|
def sort_dna_strings_gpu(sequences: List[str]) -> Optional[List[str]]:
|
|
"""Sort DNA strings on GPU using CuPy.
|
|
|
|
Encodes each string as a fixed-length byte array, sorts using
|
|
CuPy's lexicographic sort, and decodes back.
|
|
|
|
Returns None if GPU is not available.
|
|
"""
|
|
cp = try_import_cupy()
|
|
if cp is None:
|
|
return None
|
|
|
|
if not sequences:
|
|
return []
|
|
|
|
# Encode strings as byte arrays (pad to max length)
|
|
max_len = max(len(s) for s in sequences)
|
|
n = len(sequences)
|
|
|
|
# Create byte array: each row is a string, padded with spaces
|
|
byte_array = cp.zeros((n, max_len), dtype=cp.uint8)
|
|
for i, s in enumerate(sequences):
|
|
for j, c in enumerate(s):
|
|
byte_array[i, j] = ord(c)
|
|
|
|
# Sort lexicographically using CuPy's sort
|
|
# CuPy doesn't have direct string sort, but we can sort by
|
|
# converting to a single integer representation
|
|
# For short strings (≤8 bases), we can use a single uint64
|
|
if max_len <= 8:
|
|
# Pack each string into a uint64
|
|
keys = cp.zeros(n, dtype=cp.uint64)
|
|
for j in range(max_len):
|
|
keys = keys * 256 + byte_array[:, j].astype(cp.uint64)
|
|
sorted_indices = cp.argsort(keys)
|
|
else:
|
|
# For longer strings, sort column by column (radix sort)
|
|
sorted_indices = cp.arange(n, dtype=cp.int64)
|
|
for j in range(max_len - 1, -1, -1):
|
|
col = byte_array[sorted_indices, j]
|
|
order = cp.argsort(col, stable=True)
|
|
sorted_indices = sorted_indices[order]
|
|
|
|
# Reorder sequences
|
|
sorted_indices_cpu = sorted_indices.get()
|
|
return [sequences[i] for i in sorted_indices_cpu]
|
|
|
|
|
|
def sort_dna_strings(sequences: List[str], use_gpu: bool = True) -> Tuple[List[str], str]:
|
|
"""Sort DNA strings, preferring GPU if available.
|
|
|
|
Returns:
|
|
(sorted_sequences, device_used)
|
|
"""
|
|
if use_gpu:
|
|
gpu_result = sort_dna_strings_gpu(sequences)
|
|
if gpu_result is not None:
|
|
return gpu_result, "gpu"
|
|
|
|
return sort_dna_strings_cpu(sequences), "cpu"
|
|
|
|
|
|
# ============================================================
|
|
# §5 THE SMUGGLE: QUBO → DNA → GPU SORT → SOLUTIONS
|
|
# ============================================================
|
|
|
|
@dataclass
|
|
class SmuggleResult:
|
|
"""Result of smuggling a QUBO through a GPU sort."""
|
|
n_vars: int
|
|
n_solutions: int
|
|
device: str
|
|
encode_time: float
|
|
sort_time: float
|
|
decode_time: float
|
|
total_time: float
|
|
optimal: DNASolution
|
|
worst: DNASolution
|
|
sorted_solutions: List[DNASolution]
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"n_vars": self.n_vars,
|
|
"n_solutions": self.n_solutions,
|
|
"device": self.device,
|
|
"encode_time": round(self.encode_time, 4),
|
|
"sort_time": round(self.sort_time, 4),
|
|
"decode_time": round(self.decode_time, 4),
|
|
"total_time": round(self.total_time, 4),
|
|
"optimal": self.optimal.to_dict(),
|
|
"worst": self.worst.to_dict(),
|
|
}
|
|
|
|
|
|
def smuggle_qubo(
|
|
Q: List[List[float]],
|
|
n_vars: int,
|
|
n_samples: int = 0,
|
|
use_gpu: bool = True,
|
|
seed: int = 42,
|
|
) -> SmuggleResult:
|
|
"""Smuggle a QUBO problem through a GPU sort.
|
|
|
|
The GPU thinks it's sorting strings.
|
|
It's actually finding the optimal QUBO solution.
|
|
|
|
Args:
|
|
Q: QUBO matrix
|
|
n_vars: number of variables
|
|
n_samples: 0 for brute force, else sampling
|
|
use_gpu: whether to try GPU acceleration
|
|
seed: RNG seed for sampling
|
|
|
|
Returns:
|
|
SmuggleResult with timing and solutions
|
|
"""
|
|
t_total = time.time()
|
|
|
|
# Step 1: Encode solutions as DNA (CPU)
|
|
t0 = time.time()
|
|
if n_samples == 0 and n_vars <= 20:
|
|
solutions = encode_all_solutions(Q, n_vars)
|
|
else:
|
|
solutions = encode_sampled_solutions(Q, n_vars, n_samples or 50000, seed)
|
|
t_encode = time.time() - t0
|
|
|
|
# Step 2: Extract DNA sequences
|
|
sequences = [s.sequence for s in solutions]
|
|
|
|
# Step 3: Sort DNA strings (GPU or CPU)
|
|
t0 = time.time()
|
|
sorted_seqs, device = sort_dna_strings(sequences, use_gpu)
|
|
t_sort = time.time() - t0
|
|
|
|
# Step 4: Reconstruct sorted solutions
|
|
t0 = time.time()
|
|
seq_to_solution = {s.sequence: s for s in solutions}
|
|
sorted_solutions = [seq_to_solution[seq] for seq in sorted_seqs]
|
|
t_decode = time.time() - t0
|
|
|
|
t_total_elapsed = time.time() - t_total
|
|
|
|
return SmuggleResult(
|
|
n_vars=n_vars,
|
|
n_solutions=len(solutions),
|
|
device=device,
|
|
encode_time=t_encode,
|
|
sort_time=t_sort,
|
|
decode_time=t_decode,
|
|
total_time=t_total_elapsed,
|
|
optimal=sorted_solutions[0],
|
|
worst=sorted_solutions[-1],
|
|
sorted_solutions=sorted_solutions,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# §6 DEMO
|
|
# ============================================================
|
|
|
|
def demo_qubo(n: int = 10, seed: int = 42, style: str = "banded") -> List[List[float]]:
|
|
"""Generate a demo QUBO."""
|
|
rng = random.Random(seed)
|
|
Q = [[0.0] * n for _ in range(n)]
|
|
|
|
if style == "banded":
|
|
for i in range(n):
|
|
Q[i][i] = rng.uniform(2.0, 8.0)
|
|
if i + 1 < n:
|
|
c = rng.uniform(-3.0, -0.5)
|
|
Q[i][i + 1] = c
|
|
Q[i + 1][i] = c
|
|
elif style == "ising":
|
|
for i in range(n):
|
|
Q[i][i] = -1.0
|
|
if i + 1 < n:
|
|
Q[i][i + 1] = -0.5
|
|
Q[i + 1][i] = -0.5
|
|
else:
|
|
for i in range(n):
|
|
Q[i][i] = rng.uniform(-2, 5)
|
|
for j in range(i + 1, n):
|
|
c = rng.uniform(-2, 2)
|
|
Q[i][j] = c
|
|
Q[j][i] = c
|
|
|
|
return Q
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print("DNA Smuggle: QUBO → GPU Sort → Solutions")
|
|
print("=" * 60)
|
|
|
|
# Check GPU availability
|
|
cp = try_import_cupy()
|
|
if cp is not None:
|
|
print(f"GPU: {cp.cuda.runtime.getDeviceProperties(0)['name'].decode()}")
|
|
else:
|
|
print("GPU: not available (CuPy not installed), using CPU")
|
|
|
|
# Demo 1: Small (brute force)
|
|
print(f"\n--- Demo 1: 12-variable banded QUBO (brute force) ---")
|
|
Q1 = demo_qubo(12, seed=42, style="banded")
|
|
r1 = smuggle_qubo(Q1, 12, use_gpu=True)
|
|
print(f" Solutions: {r1.n_solutions:,}")
|
|
print(f" Device: {r1.device}")
|
|
print(f" Encode: {r1.encode_time:.3f}s")
|
|
print(f" Sort: {r1.sort_time:.3f}s")
|
|
print(f" Total: {r1.total_time:.3f}s")
|
|
print(f" Optimal: x={r1.optimal.solution[:8]}..., E={r1.optimal.energy:.4f}")
|
|
print(f" Worst: x={r1.worst.solution[:8]}..., E={r1.worst.energy:.4f}")
|
|
|
|
# Demo 2: Medium (brute force, 2^16 = 65K)
|
|
print(f"\n--- Demo 2: 16-variable banded QUBO (brute force) ---")
|
|
Q2 = demo_qubo(16, seed=42, style="banded")
|
|
r2 = smuggle_qubo(Q2, 16, use_gpu=True)
|
|
print(f" Solutions: {r2.n_solutions:,}")
|
|
print(f" Device: {r2.device}")
|
|
print(f" Encode: {r2.encode_time:.3f}s")
|
|
print(f" Sort: {r2.sort_time:.3f}s")
|
|
print(f" Total: {r2.total_time:.3f}s")
|
|
print(f" Optimal: x={r2.optimal.solution[:8]}..., E={r2.optimal.energy:.4f}")
|
|
|
|
# Demo 3: Large (sampling)
|
|
print(f"\n--- Demo 3: 20-variable banded QUBO (50K samples) ---")
|
|
Q3 = demo_qubo(20, seed=42, style="banded")
|
|
r3 = smuggle_qubo(Q3, 20, n_samples=50000, use_gpu=True)
|
|
print(f" Solutions: {r3.n_solutions:,} (sampled from 2^20 = {2**20:,})")
|
|
print(f" Device: {r3.device}")
|
|
print(f" Encode: {r3.encode_time:.3f}s")
|
|
print(f" Sort: {r3.sort_time:.3f}s")
|
|
print(f" Total: {r3.total_time:.3f}s")
|
|
print(f" Optimal: x={r3.optimal.solution[:8]}..., E={r3.optimal.energy:.4f}")
|
|
|
|
# Demo 4: Very large (sampling)
|
|
print(f"\n--- Demo 4: 24-variable banded QUBO (100K samples) ---")
|
|
Q4 = demo_qubo(24, seed=42, style="banded")
|
|
r4 = smuggle_qubo(Q4, 24, n_samples=100000, use_gpu=True)
|
|
print(f" Solutions: {r4.n_solutions:,} (sampled from 2^24 = {2**24:,})")
|
|
print(f" Device: {r4.device}")
|
|
print(f" Encode: {r4.encode_time:.3f}s")
|
|
print(f" Sort: {r4.sort_time:.3f}s")
|
|
print(f" Total: {r4.total_time:.3f}s")
|
|
print(f" Optimal: x={r4.optimal.solution[:8]}..., E={r4.optimal.energy:.4f}")
|
|
|
|
print(f"\n{'='*60}")
|
|
print("DONE")
|
|
print(f"{'='*60}")
|