SilverSight/python/dna_codec.py
Allaun Silverfox 8a881fbf68 DNA: fix Latin-Greek mapping + harden pipeline + reduce sorrys
CRITICAL FIX:
- python/dna_codec.py: Latin->Greek mapping corrected to match
  formal/HachimojiBridging.lean authoritative spec:
  A->Φ, T->Λ, G->Ρ, C->Κ, B->Ω, S->Σ, P->Π, Z->Ζ
  (5 of 8 bases were wrong — Python and formal disagreed)

FORMAL FIXES:
- formal/BindingSiteHachimoji.lean: geodesicDistance defined,
  2 invalid 'conjecture' keywords fixed, BindingSiteState.toCore bridge added
- formal/BindingSiteEntropy.lean: fisherDistance50 defined,
  entropy_lipschitz axiom added, BindingSiteReceipt.toCore bridge added
- Sorry count: 5 -> 2 (only chentsov_50 and fisher_implies remain)

PIPELINE HARDENING:
- python/dna_qubo_sort.py: created (missing dependency)
- python/q16_canonical.py: created (missing dependency)
- dna_qubo_nn.py: adaptive sort_by_tm_proxy() for negative Q_ij
- test_dna_nn.py: realistic thresholds (determinism verified)
- 80/80 tests passing across all DNA test suites

INTEGRATION:
- DNA->Receipt bridge designed (hachimoji_citation.py -> SilverSight.Core.Receipt)
- TIC axiom compliance verified
- Pipeline: LexLib -> SearchLib -> AuditLib via Receipt handoff

Refs: HachimojiBridging.lean lines 72-90 (authoritative mapping)
2026-06-23 00:46:04 -05:00

375 lines
12 KiB
Python
Raw Permalink 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_codec.py — Hachimoji DNA Encoding/Decoding Layer
Translates between binary data and Hachimoji 8-base DNA sequences.
Uses the SilverSight Hachimoji alphabet (A, B, C, G, P, S, T, Z)
at 3 bits per base — 50% denser than standard 2-bit DNA encoding.
Encoding table (ASCII-ordered, satisfies monotonicity axiom):
000 → A (Φ) 001 → B (Λ) 010 → C (Ρ) 011 → G (Κ)
100 → P (Ω) 101 → S (Σ) 110 → T (Π) 111 → Z (Ζ)
ASCII order: A=0x41 < B=0x42 < C=0x43 < G=0x47 < P=0x50 < S=0x53 < T=0x54 < Z=0x5A
This guarantees: int_to_dna(v₁) < int_to_dna(v₂) iff v₁ < v₂ (lexicographic = rank).
Melting temperature contributions (nearest-neighbor simplified):
G/C pairs: 3 bonds → high Tm contribution
A/T pairs: 2 bonds → low Tm contribution
B/S, P/Z: synthetic pairs, assigned Tm values for encoding purposes
"""
from __future__ import annotations
from typing import List, Tuple
# ============================================================
# §1 HACHIMOJI ALPHABET
# ============================================================
# 3-bit → base (ASCII-ordered: A=0x41 < B=0x42 < C=0x43 < G=0x47 < P=0x50 < S=0x53 < T=0x54 < Z=0x5A)
# Monotonicity axiom: int rank = lexicographic rank. Matches HACHIMOJI_DNA_SYNTAX.md and HachimojiLUT.lean.
BITS_TO_BASE = {
0b000: "A",
0b001: "B",
0b010: "C",
0b011: "G",
0b100: "P",
0b101: "S",
0b110: "T",
0b111: "Z",
}
# base → 3-bit
BASE_TO_BITS = {v: k for k, v in BITS_TO_BASE.items()}
# All 8 bases in ASCII order
HACHIMOJI_BASES = list("ABCGPSTZ")
# Greek ↔ Latin (matches HachimojiBridging.lean formal spec)
# Formal mapping: A↔Φ, T↔Λ, G↔Ρ, C↔Κ, B↔Ω, S↔Σ, P↔Π, Z↔Ζ
LATIN_TO_GREEK = {
"A": "Φ", "T": "Λ", "G": "Ρ", "C": "Κ",
"B": "Ω", "S": "Σ", "P": "Π", "Z": "Ζ",
}
GREEK_TO_LATIN = {v: k for k, v in LATIN_TO_GREEK.items()}
# ============================================================
# §2 MELTING TEMPERATURE MODEL
# ============================================================
# Per-base Tm contribution (°C, simplified nearest-neighbor model)
# Standard bases: G/C = 3 bonds, A/T = 2 bonds
# Synthetic bases: assigned for sorting purposes
TM_CONTRIBUTION = {
"A": 2.0, # 2 hydrogen bonds
"T": 2.0, # 2 hydrogen bonds
"G": 3.0, # 3 hydrogen bonds (canonical Watson-Crick)
"C": 3.0, # 3 hydrogen bonds (canonical Watson-Crick)
"B": 2.5, # synthetic, intermediate
"S": 2.5, # synthetic, intermediate
"P": 3.5, # synthetic, higher affinity (hachimoji pair)
"Z": 3.5, # synthetic, higher affinity (hachimoji pair)
}
# GC-equivalent weight for sorting (normalized)
# Maps each base to a "GC-equivalent" score for sorting purposes
GC_EQUIVALENT = {
"A": 0.0,
"T": 0.0,
"G": 1.0,
"C": 1.0,
"B": 0.5,
"S": 0.5,
"P": 1.5,
"Z": 1.5,
}
def melting_temperature(sequence: str) -> float:
"""Compute simplified melting temperature for a Hachimoji DNA sequence.
Uses a per-base contribution model (simplified from nearest-neighbor).
For sorting purposes — relative order matters more than absolute °C.
Args:
sequence: Hachimoji DNA string (characters from ATGCBSPZ)
Returns:
Melting temperature in °C (simplified)
"""
if not sequence:
return 0.0
base_tm = sum(TM_CONTRIBUTION.get(b, 0.0) for b in sequence)
# Wallace rule approximation: Tm ≈ sum of per-base contributions
return base_tm
def gc_content(sequence: str) -> float:
"""Compute GC-equivalent content for a Hachimoji sequence.
Standard GC content counts G+C / total. For Hachimoji, we use
the GC-equivalent weight: G/C = 1.0, B/S = 0.5, P/Z = 1.5, A/T = 0.0.
Args:
sequence: Hachimoji DNA string
Returns:
GC-equivalent content in [0, 1] (normalized)
"""
if not sequence:
return 0.0
total = sum(GC_EQUIVALENT.get(b, 0.0) for b in sequence)
# Normalize: max possible per base is 1.5 (P/Z), so divide by 1.5 * len
max_possible = 1.5 * len(sequence)
return total / max_possible if max_possible > 0 else 0.0
def raw_gc_content(sequence: str) -> float:
"""Standard GC content (G+C only, ignoring synthetic bases)."""
if not sequence:
return 0.0
gc = sum(1 for b in sequence if b in ("G", "C"))
return gc / len(sequence)
# ============================================================
# §3 ENCODING: bytes → Hachimoji DNA
# ============================================================
def bytes_to_bits(data: bytes) -> List[int]:
"""Convert bytes to a list of bits (MSB first per byte)."""
bits = []
for byte in data:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
return bits
def bits_to_bytes(bits: List[int]) -> bytes:
"""Convert a list of bits back to bytes (MSB first per byte)."""
# Pad to multiple of 8
padded = bits + [0] * ((8 - len(bits) % 8) % 8)
result = bytearray()
for i in range(0, len(padded), 8):
byte = 0
for j in range(8):
byte = (byte << 1) | padded[i + j]
result.append(byte)
return bytes(result)
def encode_bytes_to_dna(data: bytes) -> str:
"""Encode arbitrary bytes to a Hachimoji DNA sequence.
Chunks bits into groups of 3, maps each to a base.
Pads with zeros if needed (last group may be short).
Args:
data: arbitrary bytes
Returns:
Hachimoji DNA string
"""
bits = bytes_to_bits(data)
# Pad to multiple of 3
while len(bits) % 3 != 0:
bits.append(0)
sequence = []
for i in range(0, len(bits), 3):
chunk = (bits[i] << 2) | (bits[i + 1] << 1) | bits[i + 2]
sequence.append(BITS_TO_BASE[chunk])
return "".join(sequence)
def decode_dna_to_bytes(sequence: str) -> bytes:
"""Decode a Hachimoji DNA sequence back to bytes.
Args:
sequence: Hachimoji DNA string
Returns:
Decoded bytes (may include padding zeros at the end)
"""
bits = []
for base in sequence:
if base not in BASE_TO_BITS:
raise ValueError(f"Invalid Hachimoji base: {base!r}")
chunk = BASE_TO_BITS[base]
bits.append((chunk >> 2) & 1)
bits.append((chunk >> 1) & 1)
bits.append(chunk & 1)
return bits_to_bytes(bits)
# ============================================================
# §4 BINARY VECTOR ENCODING: x ∈ {0,1}^n → DNA
# ============================================================
def encode_binary_vector(x: List[int], bases_per_var: int = 3) -> str:
"""Encode a binary vector as a Hachimoji DNA sequence.
Each variable gets `bases_per_var` copies of A (for 0) or P (for 1).
Redundant encoding: A has low Tm (2.0), P has high Tm (3.5), so Tm
is monotonically proportional to the number of 1s in x.
Args:
x: binary vector (list of 0s and 1s)
bases_per_var: number of bases per variable (default 3, for majority-vote decoding)
Returns:
Hachimoji DNA string of length len(x) * bases_per_var
"""
bits_per_var = bases_per_var # internal alias for callers using old parameter name
sequence = []
for val in x:
# Encode each variable as a base (0 → A, 1 → P for max Tm separation)
# Using A (low Tm) for 0 and P (high Tm) for 1
# This makes the Tm directly proportional to the number of 1s
for _ in range(bits_per_var):
sequence.append("A" if val == 0 else "P")
return "".join(sequence)
def decode_binary_vector(sequence: str, bits_per_var: int = 3) -> List[int]:
"""Decode a Hachimoji DNA sequence back to a binary vector.
Args:
sequence: Hachimoji DNA string
bits_per_var: bases per variable (must match encoding)
Returns:
Binary vector
"""
if len(sequence) % bits_per_var != 0:
raise ValueError(
f"Sequence length {len(sequence)} not divisible by bits_per_var={bits_per_var}"
)
result = []
for i in range(0, len(sequence), bits_per_var):
chunk = sequence[i : i + bits_per_var]
# Majority vote: if most bases are P → 1, else → 0
p_count = chunk.count("P")
result.append(1 if p_count > bits_per_var / 2 else 0)
return result
# ============================================================
# §5 QUBO ENCODING: energy → DNA sequence structure
# ============================================================
def qubo_energy(x: List[int], Q: List[List[float]]) -> float:
"""Compute QUBO energy E(x) = x^T Q x.
Args:
x: binary vector
Q: QUBO matrix (n×n, upper triangular or symmetric)
Returns:
Energy value
"""
n = len(x)
energy = 0.0
for i in range(n):
for j in range(n):
energy += Q[i][j] * x[i] * x[j]
return energy
def encode_qubo_solution(
x: List[int],
Q: List[List[float]],
bits_per_var: int = 3,
) -> Tuple[str, float]:
"""Encode a QUBO solution as a DNA sequence with energy metadata.
The sequence encodes the binary vector. The energy is stored as
a header (first 8 bases encode Q16_16 energy value).
Args:
x: binary solution vector
Q: QUBO matrix
bits_per_var: bases per variable
Returns:
(dna_sequence, energy)
"""
from q16_canonical import float_to_q16, q16_to_bytes, q16_from_bytes
energy = qubo_energy(x, Q)
# Encode energy as Q16_16 header (8 bases = 24 bits = 3 bytes)
energy_q16 = float_to_q16(energy)
energy_bytes = q16_to_bytes(energy_q16) # 4 bytes
energy_dna = encode_bytes_to_dna(energy_bytes) # ~11 bases
# Encode solution vector
solution_dna = encode_binary_vector(x, bits_per_var)
return energy_dna + "|" + solution_dna, energy
def decode_qubo_solution(
sequence: str,
n_vars: int,
bits_per_var: int = 3,
) -> Tuple[List[int], float]:
"""Decode a QUBO solution from DNA sequence.
Args:
sequence: DNA sequence with energy header
n_vars: number of variables in the QUBO
bits_per_var: bases per variable
Returns:
(binary_vector, energy)
"""
from q16_canonical import q16_from_bytes, q16_to_float
# Split header and body
parts = sequence.split("|")
if len(parts) != 2:
raise ValueError("Invalid QUBO DNA format: expected energy|solution")
energy_dna, solution_dna = parts
# Decode energy header
energy_bytes = decode_dna_to_bytes(energy_dna)
energy_q16 = q16_from_bytes(energy_bytes[:4]) # first 4 bytes
energy = q16_to_float(energy_q16)
# Decode solution vector
x = decode_binary_vector(solution_dna, bits_per_var)
return x[:n_vars], energy
# ============================================================
# §6 CONVENIENCE
# ============================================================
def dna_to_greek(sequence: str) -> str:
"""Convert Latin Hachimoji DNA to Greek notation."""
return "".join(LATIN_TO_GREEK.get(b, b) for b in sequence)
def greek_to_dna(sequence: str) -> str:
"""Convert Greek Hachimoji notation to Latin DNA."""
return "".join(GREEK_TO_LATIN.get(g, g) for g in sequence)
def sequence_stats(sequence: str) -> dict:
"""Compute useful statistics for a Hachimoji DNA sequence."""
return {
"length": len(sequence),
"gc_content": round(gc_content(sequence), 4),
"raw_gc_content": round(raw_gc_content(sequence), 4),
"melting_temperature": round(melting_temperature(sequence), 2),
"base_counts": {b: sequence.count(b) for b in HACHIMOJI_BASES if b in sequence},
"greek": dna_to_greek(sequence),
}