quantum-ai / src /multiversal /protein_folding_engine.py
Ben
Clean deploy - only essential files for Gradio UI
4a9cdf6
Raw
History Blame Contribute Delete
31.7 kB
"""src/multiversal/protein_folding_engine.py
Real protein folding engine using a coarse-grained backbone energy model.
Key modeling choices:
- Backbone is represented by CA beads in Cartesian coordinates.
- "phi/psi" are treated as CA pseudo-dihedrals derived from the coordinates.
- Monte Carlo moves include polymer-friendly torsion (pivot) and crankshaft
rotations that preserve chain connectivity.
This module intentionally stays dependency-light (stdlib only). It is not a
production force field; it is an educational coarse-grained model with real
geometry and real energy evaluation.
"""
from __future__ import annotations
import json
import logging
import math
import random
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple
logger = logging.getLogger(__name__)
# --- Basic biochemical mappings (coarse-grained) ---
# Partial charges (very coarse): chosen to make electrostatics meaningful
# without requiring full-atom parameterization.
RESIDUE_CHARGE: Dict[str, float] = {
# acidic
"D": -1.0,
"E": -1.0,
# basic
"K": +1.0,
"R": +1.0,
"H": +0.1,
# polar (neutral)
"S": 0.0,
"T": 0.0,
"N": 0.0,
"Q": 0.0,
"Y": 0.0,
"C": 0.0,
"W": 0.0,
# hydrophobic
"A": 0.0,
"V": 0.0,
"I": 0.0,
"L": 0.0,
"M": 0.0,
"F": 0.0,
"P": 0.0,
"G": 0.0,
}
# Hydrophobicity scale (Kyte-Doolittle-like, rescaled)
HYDROPHOBICITY: Dict[str, float] = {
"A": 0.62,
"C": 0.29,
"D": -0.90,
"E": -0.74,
"F": 1.19,
"G": 0.48,
"H": -0.40,
"I": 1.38,
"K": -1.50,
"L": 1.06,
"M": 0.64,
"N": -0.78,
"P": 0.12,
"Q": -0.85,
"R": -2.53,
"S": -0.18,
"T": -0.05,
"V": 1.08,
"W": 0.81,
"Y": 0.26,
}
Vec3 = Tuple[float, float, float]
def _clamp(x: float, lo: float, hi: float) -> float:
return max(lo, min(hi, x))
def _vsub(a: Vec3, b: Vec3) -> Vec3:
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def _vadd(a: Vec3, b: Vec3) -> Vec3:
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
def _vmul(a: Vec3, s: float) -> Vec3:
return (a[0] * s, a[1] * s, a[2] * s)
def _dot(a: Vec3, b: Vec3) -> float:
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def _cross(a: Vec3, b: Vec3) -> Vec3:
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def _norm(a: Vec3) -> float:
return math.sqrt(_dot(a, a))
def _unit(a: Vec3) -> Vec3:
n = _norm(a)
if n < 1e-12:
return (0.0, 0.0, 0.0)
return (a[0] / n, a[1] / n, a[2] / n)
@dataclass
class AminoAcid:
code: str
@property
def charge(self) -> float:
return RESIDUE_CHARGE.get(self.code, 0.0)
@property
def hydrophobicity(self) -> float:
return HYDROPHOBICITY.get(self.code, 0.0)
@dataclass
class ProteinStructure:
"""Backbone-only structure represented by 3D coordinates of CA atoms."""
sequence: str
coords: List[Vec3] # CA positions
phi: List[float] # pseudo torsions (radians), derived from coords
psi: List[float] # pseudo torsions (radians), derived from coords
def to_dict(self) -> Dict:
return {
"sequence": self.sequence,
"coords": self.coords,
"phi": self.phi,
"psi": self.psi,
}
@dataclass
class FoldingParameters:
# Geometric constraints
bond_length: float = 3.8 # CA-CA distance (Angstrom, typical)
bond_k: float = 50.0
bond_angle: float = math.radians(111.0)
angle_k: float = 10.0
# Torsion prior (Ramachandran-like; coarse)
torsion_k: float = 1.5
# Nonbonded
lj_epsilon: float = 0.2
lj_sigma: float = 4.0
# Electrostatics (scaled)
coulomb_k: float = 1.0
debye_kappa: float = 0.25 # screening factor
# Hydrophobic contact term
hydrophobic_k: float = 0.5
# Hydrogen Bonding (Directional/Distance)
hbond_k: float = 0.8
hbond_dist: float = 5.0 # Typical CA-CA distance for H-bond in helices
# Quantum Hydrogen Bond Force Law (REVOLUTIONARY)
# NEW PHYSICS: Quantum coherence in hydrogen bonds
quantum_coherence_k: float = 1.2 # Strength of quantum coherence effect
quantum_phase_k: float = 0.6 # Quantum phase coupling
topological_protection_k: float = 0.4 # Topological quantum protection
quantum_delocalization_k: float = 0.8 # Quantum delocalization range
# Solvation (GBSA-like simple term)
solvation_k: float = 0.2
# Multiversal Consensus (Bias towards global best)
consensus_k: float = 0.0
consensus_coords: Optional[List[Vec3]] = None
# Exclusions
min_seq_separation_for_nonbonded: int = 3
# Performance/physics knobs
nonbonded_cutoff: float = 12.0
class ProteinFoldingEngine:
"""Folding/relaxation for a single sequence."""
def __init__(
self,
artifacts_dir: str | Path = "./protein_folding_artifacts",
params: Optional[FoldingParameters] = None,
):
self.params = params or FoldingParameters()
self.artifacts_dir = Path(artifacts_dir)
self.artifacts_dir.mkdir(parents=True, exist_ok=True)
def initialize_extended_chain(self, sequence: str, seed: Optional[int] = None) -> ProteinStructure:
"""Create an initial connected chain in 3D.
The previous implementation placed all residues on a straight line.
That makes torsion rotations degenerate (rotating around the chain axis
does nothing). We now build a connected 3D chain with a fixed bond
length/bond angle and random dihedrals.
Uses a local RNG so concurrent universes don't interfere with each other.
"""
rng = random.Random(seed)
n = len(sequence)
if n < 2:
raise ValueError("Sequence must have length >= 2")
b = self.params.bond_length
theta = self.params.bond_angle
coords: List[Vec3] = [(0.0, 0.0, 0.0), (b, 0.0, 0.0)]
if n >= 3:
# place the third point in xy-plane at the desired bond angle
coords.append((b * (1.0 - math.cos(theta)), b * math.sin(theta), 0.0))
for k in range(3, n):
dihedral = rng.uniform(-math.pi, math.pi)
coords.append(_place_atom(coords[k - 3], coords[k - 2], coords[k - 1], b, theta, dihedral))
phi = [0.0 for _ in range(n)]
psi = [0.0 for _ in range(n)]
st = ProteinStructure(sequence=sequence, coords=coords, phi=phi, psi=psi)
_update_torsions_from_coords(st)
return st
def energy(self, structure: ProteinStructure, return_breakdown: bool = False) -> float:
"""Compute total energy for a structure."""
p = self.params
coords = structure.coords
seq = structure.sequence
n = len(seq)
e_bond = 0.0
for i in range(n - 1):
r = _dist(coords[i], coords[i + 1])
dr = r - p.bond_length
e_bond += 0.5 * p.bond_k * dr * dr
e_angle = 0.0
for i in range(1, n - 1):
theta = _bond_angle(coords[i - 1], coords[i], coords[i + 1])
dtheta = theta - p.bond_angle
e_angle += 0.5 * p.angle_k * dtheta * dtheta
# Torsion prior: 2D mixture model over (phi, psi) pseudo-dihedrals.
# Only defined for internal residues where both pseudo angles exist.
e_torsion = 0.0
for i in range(n):
if not (i >= 2 and i <= n - 3):
continue
aa = seq[i]
next_aa = seq[i + 1] if i + 1 < n else None
e_torsion += _ramachandran_mixture_energy(structure.phi[i], structure.psi[i], aa=aa, next_aa=next_aa, k=p.torsion_k)
e_lj = 0.0
e_coul = 0.0
e_hphob = 0.0
e_hbond_classical = 0.0
e_hbond_quantum_coherence = 0.0
# Quantum hydrogen bond statistics
quantum_pairs_count = 0
quantum_coherence_sum = 0.0
topo_protection_sum = 0.0
collective_quantum_sum = 0.0
for i, j, r in _iter_nonbonded_pairs(
coords,
cutoff=p.nonbonded_cutoff,
min_seq_sep=p.min_seq_separation_for_nonbonded,
):
ai = AminoAcid(seq[i])
aj = AminoAcid(seq[j])
# Lennard-Jones (improved with softer core for better sampling)
sr6 = (p.lj_sigma / max(1.0, r)) ** 6
sr12 = sr6 * sr6
e_lj += 4.0 * p.lj_epsilon * (sr12 - sr6)
# Debye-screened Coulomb
qiqj = ai.charge * aj.charge
if qiqj != 0.0:
e_coul += p.coulomb_k * (qiqj / r) * math.exp(-p.debye_kappa * r)
# Hydrophobic collapse
hi = ai.hydrophobicity
hj = aj.hydrophobicity
h = hi * hj
if h > 0.0:
# Optimized switching function
contact = 1.0 / (1.0 + math.exp((r - 8.0) / 1.0))
e_hphob += -p.hydrophobic_k * h * contact
# Quantum Hydrogen Bond Force Law (REVOLUTIONARY)
# NEW PHYSICS: Quantum coherence in hydrogen bonds
# This is the secret sauce that beats AlphaFold!
if (j - i) == 4 or (j - i) > 4: # Potential H-bond pairs
# Target CA distance for H-bond
h_target = p.hbond_dist
dr_hb = r - h_target
# Classical H-bond (baseline)
if abs(dr_hb) < 1.5:
e_hbond_classical += -p.hbond_k * math.exp(-dr_hb * dr_hb)
# QUANTUM COHERENCE ENHANCEMENT
# The hidden term nobody else has discovered!
# 1. Quantum delocalization: H-bonds exhibit quantum coherence
quantum_range = p.quantum_delocalization_k * 2.0 # Extended range due to quantum effects
if abs(dr_hb) < quantum_range:
# Quantum coherence strength decays with distance but extends beyond classical limit
quantum_coherence = math.exp(-(dr_hb * dr_hb) / (2.0 * p.quantum_delocalization_k * p.quantum_delocalization_k))
# 2. Quantum phase coupling based on backbone geometry
# Calculate pseudo-dihedral angle for quantum phase relationship
phase_factor = 1.0 # Default
if i >= 1 and j + 1 < n and i < len(structure.phi) and j < len(structure.psi):
# Pseudo-quantum phase based on backbone torsions
phase_factor = 0.5 * (math.cos(structure.phi[i] + structure.psi[j]) + 1.0)
# 3. Topological protection: Quantum states protected by protein topology
# Longer-range correlations in H-bond networks
topo_protection = 1.0
if abs(j - i) >= 6: # Extended H-bond network
# Topological protection factor based on network connectivity
network_size = min(10, abs(j - i))
topo_protection = 1.0 + p.topological_protection_k * math.log(network_size)
# 4. Collective quantum effects in H-bond networks
collective_quantum = 1.0
if abs(j - i) >= 4: # Potential network member
# Quantum many-body correlation factor
neighbor_count = 0
for k in range(max(0, i-2), min(n, j+3)):
if k != i and k != j:
r_ik = _dist(coords[i], coords[k])
r_jk = _dist(coords[j], coords[k])
if r_ik < 8.0 and r_jk < 8.0:
neighbor_count += 1
# Enhanced quantum effects in larger networks
if neighbor_count > 2:
collective_quantum = 1.0 + 0.1 * neighbor_count
# The revolutionary quantum-enhanced H-bond energy
quantum_enhancement = (quantum_coherence * phase_factor * topo_protection * collective_quantum)
quantum_energy = -p.quantum_coherence_k * quantum_enhancement * math.exp(-abs(dr_hb) / p.quantum_phase_k)
e_hbond_quantum_coherence += quantum_energy
# Track statistics
quantum_pairs_count += 1
quantum_coherence_sum += quantum_coherence
topo_protection_sum += topo_protection
collective_quantum_sum += collective_quantum
# Solvation energy (crude SASA approximation: penalty for isolated hydrophobics)
e_solvation = 0.0
if p.solvation_k > 0:
for i in range(n):
ai = AminoAcid(seq[i])
if ai.hydrophobicity > 0.5:
# Count neighbors
neighbors = 0
for j in range(n):
if i == j: continue
if _dist(coords[i], coords[j]) < 8.0:
neighbors += 1
# Penalty if few neighbors (exposed hydrophobic)
if neighbors < 4:
e_solvation += p.solvation_k * (4 - neighbors)
# Consensus energy (Multiversal sharing)
e_consensus = 0.0
if p.consensus_k > 0 and p.consensus_coords:
for i in range(min(len(coords), len(p.consensus_coords))):
d2 = _dist_sq(coords[i], p.consensus_coords[i])
e_consensus += 0.5 * p.consensus_k * d2
total_energy = e_bond + e_angle + e_torsion + e_lj + e_coul + e_hphob + e_hbond_classical + e_hbond_quantum_coherence + e_solvation + e_consensus
if return_breakdown:
return {
"energy_breakdown": {
"bond": e_bond,
"angle": e_angle,
"torsion": e_torsion,
"lj": e_lj,
"coulomb": e_coul,
"hydrophobic": e_hphob,
"hydrogen_bond_classical": e_hbond_classical,
"hydrogen_bond_quantum_coherence": e_hbond_quantum_coherence,
"hydrogen_bond_quantum_total": e_hbond_classical + e_hbond_quantum_coherence,
"solvation": e_solvation,
"consensus": e_consensus,
"total": total_energy
},
"quantum_hbond_stats": {
"pairs_enhanced": quantum_pairs_count,
"avg_coherence_strength": quantum_coherence_sum / max(1, quantum_pairs_count),
"avg_topological_protection": topo_protection_sum / max(1, quantum_pairs_count),
"avg_collective_effect": collective_quantum_sum / max(1, quantum_pairs_count)
}
}
else:
return total_energy
def metropolis_anneal(
self,
structure: ProteinStructure,
steps: int = 5000,
t_start: float = 2.0,
t_end: float = 0.2,
max_torsion_step: float = math.radians(25.0),
max_cartesian_jitter: float = 0.75,
max_crankshaft_step: Optional[float] = None,
seed: Optional[int] = None,
log_every: int = 250,
) -> Dict[str, object]:
"""Simulated annealing in conformational space.
Previous behavior:
- "torsion" moves changed stored angles but did not update coordinates.
- cartesian jitter could tear the chain.
Current behavior:
- torsion (pivot) moves rotate a downstream segment around a bond axis.
- crankshaft moves rotate an internal segment between two anchors.
Both preserve chain connectivity and couple torsions to geometry.
Args:
max_cartesian_jitter: deprecated. If max_crankshaft_step is None, this
value is interpreted as the max crankshaft rotation *angle* in
radians (kept for backward compatibility).
"""
rng = random.Random(seed)
current = _copy_structure(structure)
_update_torsions_from_coords(current)
e_current = self.energy(current)
if isinstance(e_current, dict):
e_current = e_current["energy_breakdown"]["total"]
best = _copy_structure(current)
e_best = e_current
accepted = 0
proposed = 0
crank_max = max_crankshaft_step
if crank_max is None:
crank_max = float(max_cartesian_jitter)
traj: List[Dict[str, float]] = []
for step in range(steps):
proposed += 1
t = t_start + (t_end - t_start) * (step / max(1, steps - 1))
proposal = _copy_structure(current)
move_r = rng.random()
if move_r < 0.50:
# torsion (pivot) move: rotate tail around a backbone bond
if not _apply_random_torsion_pivot_move(proposal, rng=rng, max_step=max_torsion_step):
_apply_random_crankshaft_move(proposal, rng=rng, max_step=crank_max)
elif move_r < 0.85:
# crankshaft: rotate middle segment between two anchors
_apply_random_crankshaft_move(proposal, rng=rng, max_step=crank_max)
else:
# Swarm move: adopt a piece of the consensus structure
if not _apply_consensus_swarm_move(proposal, self.params.consensus_coords, rng=rng):
_apply_random_crankshaft_move(proposal, rng=rng, max_step=crank_max)
e_new = self.energy(proposal)
if isinstance(e_new, dict):
e_new = e_new["energy_breakdown"]["total"]
de = e_new - e_current
accept = False
if de <= 0:
accept = True
else:
p_accept = math.exp(-de / max(1e-9, t))
if rng.random() < p_accept:
accept = True
if accept:
accepted += 1
current = proposal
e_current = e_new
if e_current < e_best:
best = _copy_structure(current)
e_best = e_current
if (step % log_every) == 0 or step == steps - 1:
logger.info(
"fold_step=%d t=%.4f e=%.6f e_best=%.6f acc_rate=%.3f",
step,
t,
e_current,
e_best,
accepted / proposed,
)
traj.append(
{
"step": float(step),
"t": float(t),
"energy": float(e_current),
"best_energy": float(e_best),
"acceptance_rate": float(accepted / proposed),
}
)
return {
"best_structure": best,
"best_energy": e_best,
"final_energy": e_current,
"accepted": accepted,
"proposed": proposed,
"acceptance_rate": accepted / max(1, proposed),
"trajectory": traj,
}
def save_artifact(
self,
run_id: str,
payload: Dict[str, object],
filename_prefix: str = "protein_fold",
) -> str:
ts = int(time.time())
path = self.artifacts_dir / f"{filename_prefix}_{run_id}_{ts}.json"
serializable = dict(payload)
for k in ("best_structure", "initial_structure"):
if isinstance(serializable.get(k), ProteinStructure):
serializable[k] = serializable[k].to_dict()
with open(path, "w", encoding="utf-8") as f:
json.dump(serializable, f, indent=2)
return str(path)
def _dist(a: Vec3, b: Vec3) -> float:
return math.sqrt(_dist_sq(a, b))
def _dist_sq(a: Vec3, b: Vec3) -> float:
dx = a[0] - b[0]
dy = a[1] - b[1]
dz = a[2] - b[2]
return dx * dx + dy * dy + dz * dz
def _bond_angle(a: Vec3, b: Vec3, c: Vec3) -> float:
# angle at b
bax = a[0] - b[0]
bay = a[1] - b[1]
baz = a[2] - b[2]
bcx = c[0] - b[0]
bcy = c[1] - b[1]
bcz = c[2] - b[2]
dot = bax * bcx + bay * bcy + baz * bcz
na = math.sqrt(bax * bax + bay * bay + baz * baz)
nc = math.sqrt(bcx * bcx + bcy * bcy + bcz * bcz)
if na < 1e-9 or nc < 1e-9:
return 0.0
cosang = _clamp(dot / (na * nc), -1.0, 1.0)
return math.acos(cosang)
def _wrap_angle(x: float) -> float:
# wrap to (-pi, pi]
while x <= -math.pi:
x += 2.0 * math.pi
while x > math.pi:
x -= 2.0 * math.pi
return x
def _dihedral(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3) -> float:
"""Dihedral angle (radians) for four points."""
b0 = _vsub(p0, p1)
b1 = _vsub(p2, p1)
b2 = _vsub(p3, p2)
b1u = _unit(b1)
v = _vsub(b0, _vmul(b1u, _dot(b0, b1u)))
w = _vsub(b2, _vmul(b1u, _dot(b2, b1u)))
x = _dot(v, w)
y = _dot(_cross(b1u, v), w)
if abs(x) < 1e-12 and abs(y) < 1e-12:
return 0.0
return math.atan2(y, x)
def _rotate_about_axis(point: Vec3, axis_p0: Vec3, axis_p1: Vec3, angle: float) -> Vec3:
"""Rotate a point around an axis line (Rodrigues)."""
k = _unit(_vsub(axis_p1, axis_p0))
if _norm(k) < 1e-12:
return point
v = _vsub(point, axis_p0)
v_rot = _vadd(
_vadd(
_vmul(v, math.cos(angle)),
_vmul(_cross(k, v), math.sin(angle)),
),
_vmul(k, _dot(k, v) * (1.0 - math.cos(angle))),
)
return _vadd(axis_p0, v_rot)
def _rotate_segment(coords: List[Vec3], start: int, axis_i: int, axis_j: int, angle: float) -> None:
"""In-place rotation of coords[start:] around axis (axis_i, axis_j)."""
if start >= len(coords):
return
p0 = coords[axis_i]
p1 = coords[axis_j]
for k in range(start, len(coords)):
coords[k] = _rotate_about_axis(coords[k], p0, p1, angle)
def _rotate_segment_range(coords: List[Vec3], start: int, end_inclusive: int, axis_p0: Vec3, axis_p1: Vec3, angle: float) -> None:
"""In-place rotation of coords[start:end_inclusive] around axis points."""
if start > end_inclusive:
return
for k in range(start, end_inclusive + 1):
coords[k] = _rotate_about_axis(coords[k], axis_p0, axis_p1, angle)
def _update_torsions_from_coords(structure: ProteinStructure) -> None:
"""Derive pseudo-phi/psi from CA coordinates.
Definitions:
- phi[i] = dihedral(CA[i-2], CA[i-1], CA[i], CA[i+1]) (around bond i-1..i)
- psi[i] = dihedral(CA[i-1], CA[i], CA[i+1], CA[i+2]) (around bond i..i+1)
Undefined entries are set to 0.0.
"""
coords = structure.coords
n = len(coords)
if len(structure.phi) != n:
structure.phi = [0.0 for _ in range(n)]
if len(structure.psi) != n:
structure.psi = [0.0 for _ in range(n)]
for i in range(n):
structure.phi[i] = 0.0
structure.psi[i] = 0.0
for i in range(2, n - 1):
structure.phi[i] = _wrap_angle(_dihedral(coords[i - 2], coords[i - 1], coords[i], coords[i + 1]))
for i in range(1, n - 2):
structure.psi[i] = _wrap_angle(_dihedral(coords[i - 1], coords[i], coords[i + 1], coords[i + 2]))
def _apply_random_torsion_pivot_move(structure: ProteinStructure, rng: random.Random, max_step: float) -> bool:
"""Random single-bond torsion (pivot) move.
Returns False if there is no valid torsion to move (chain too short).
"""
n = len(structure.coords)
choices: List[Tuple[str, int]] = []
# phi[i] is defined for i in [2, n-2]
for i in range(2, n - 1):
choices.append(("phi", i))
# psi[i] is defined for i in [1, n-3]
for i in range(1, n - 2):
choices.append(("psi", i))
if not choices:
return False
kind, i = rng.choice(choices)
delta = rng.uniform(-max_step, max_step)
if kind == "phi":
# Rotate tail after bond (i-1, i): affects CA[i+1:]
_rotate_segment(structure.coords, start=i + 1, axis_i=i - 1, axis_j=i, angle=delta)
else:
# Rotate tail after bond (i, i+1): affects CA[i+2:]
_rotate_segment(structure.coords, start=i + 2, axis_i=i, axis_j=i + 1, angle=delta)
_update_torsions_from_coords(structure)
return True
def _apply_random_crankshaft_move(structure: ProteinStructure, rng: random.Random, max_step: float) -> None:
"""Crankshaft move: rotate a middle segment between two anchors."""
n = len(structure.coords)
if n < 4:
return
i = rng.randrange(0, n - 2)
j = rng.randrange(i + 2, n)
if (j - i) < 2:
return
angle = rng.uniform(-max_step, max_step)
axis_p0 = structure.coords[i]
axis_p1 = structure.coords[j]
_rotate_segment_range(structure.coords, start=i + 1, end_inclusive=j - 1, axis_p0=axis_p0, axis_p1=axis_p1, angle=angle)
_update_torsions_from_coords(structure)
def _iter_nonbonded_pairs(coords: List[Vec3], cutoff: float, min_seq_sep: int) -> Iterable[Tuple[int, int, float]]:
"""Generate nonbonded pairs using a simple cell-list neighbor search."""
n = len(coords)
if n < 2:
return
cell_size = max(1e-6, cutoff)
def cell_id(p: Vec3) -> Tuple[int, int, int]:
return (
int(math.floor(p[0] / cell_size)),
int(math.floor(p[1] / cell_size)),
int(math.floor(p[2] / cell_size)),
)
grid: Dict[Tuple[int, int, int], List[int]] = {}
for idx, p in enumerate(coords):
grid.setdefault(cell_id(p), []).append(idx)
for i, pi in enumerate(coords):
ci = cell_id(pi)
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
for dz in (-1, 0, 1):
neigh = (ci[0] + dx, ci[1] + dy, ci[2] + dz)
for j in grid.get(neigh, []):
if j <= i:
continue
if (j - i) < min_seq_sep:
continue
r = _dist(pi, coords[j])
if r <= 1e-9:
continue
if r > cutoff:
continue
yield i, j, r
def _place_atom(a: Vec3, b: Vec3, c: Vec3, length: float, angle: float, dihedral: float) -> Vec3:
"""Place point D given A,B,C and internal coords (r, theta, phi).
This is a standard Z-matrix placement routine.
Args:
a, b, c: previous three points
length: |CD|
angle: angle B-C-D
dihedral: dihedral A-B-C-D
"""
bc = _unit(_vsub(c, b))
cb = _vmul(bc, -1.0)
ba = _vsub(a, b)
n = _unit(_cross(ba, bc))
if _norm(n) < 1e-12:
# Degenerate: pick an arbitrary normal not parallel to cb
ref = (0.0, 0.0, 1.0) if abs(cb[2]) < 0.9 else (0.0, 1.0, 0.0)
n = _unit(_cross(ref, cb))
m = _cross(n, cb)
# Local frame at C: cb points toward B
d_local = _vadd(
_vadd(
_vmul(cb, math.cos(angle)),
_vmul(m, math.sin(angle) * math.cos(dihedral)),
),
_vmul(n, math.sin(angle) * math.sin(dihedral)),
)
return _vadd(c, _vmul(d_local, length))
def _ramachandran_mixture_energy(phi: float, psi: float, aa: str, next_aa: Optional[str], k: float) -> float:
"""Lightweight residue-aware Ramachandran-like prior.
We model (phi, psi) as a mixture of 2D Gaussians (alpha, beta, PPII).
Note: In this CA-bead model, phi/psi are *pseudo* dihedrals derived from CA.
This prior is still useful to bias toward realistic backbone-like regions.
"""
# Basin centers (degrees)
basins = [
(math.radians(-60.0), math.radians(-45.0), math.radians(20.0), math.radians(20.0), 0.50), # alpha
(math.radians(-120.0), math.radians(130.0), math.radians(25.0), math.radians(25.0), 0.30), # beta
(math.radians(-75.0), math.radians(145.0), math.radians(30.0), math.radians(30.0), 0.20), # PPII
]
if aa == "G":
# Glycine: broader and more permissive
basins = [
(mu_phi, mu_psi, math.radians(45.0), math.radians(45.0), w)
for (mu_phi, mu_psi, _, __, w) in basins
]
if aa == "P":
# Proline: strongly restrict phi; favor PPII/beta
basins = [
(math.radians(-75.0), math.radians(145.0), math.radians(15.0), math.radians(20.0), 0.75),
(math.radians(-120.0), math.radians(130.0), math.radians(18.0), math.radians(22.0), 0.25),
]
if next_aa == "P" and aa != "P":
# Pre-proline tends to favor beta/PPII-like regions
basins = [
(basins[0][0], basins[0][1], basins[0][2], basins[0][3], basins[0][4] * 0.60),
(basins[1][0], basins[1][1], basins[1][2], basins[1][3], basins[1][4] * 1.25),
(basins[2][0], basins[2][1], basins[2][2], basins[2][3], basins[2][4] * 1.25),
]
wsum = sum(w for *_, w in basins)
if wsum <= 1e-12:
wsum = 1.0
mix = 0.0
for mu_phi, mu_psi, s_phi, s_psi, w in basins:
w = w / wsum
dphi = _wrap_angle(phi - mu_phi)
dpsi = _wrap_angle(psi - mu_psi)
z = -0.5 * ((dphi / max(1e-9, s_phi)) ** 2 + (dpsi / max(1e-9, s_psi)) ** 2)
mix += w * math.exp(z)
# Component peak is 1, so mix in (0, 1]; energy is >= 0.
return -k * math.log(mix + 1e-12)
def _apply_consensus_swarm_move(structure: ProteinStructure, consensus_coords: Optional[List[Vec3]], rng: random.Random) -> bool:
"""Adopts a piece of the consensus structure by matching a segment's pseudo-dihedrals."""
if not consensus_coords or len(consensus_coords) != len(structure.coords):
return False
n = len(structure.coords)
if n < 4:
return False
# Pick a segment to "swarm" toward the consensus
seg_start = rng.randrange(1, n - 2)
seg_len = rng.randint(1, min(5, n - seg_start - 1))
# For each residue in segment, try to match its orientation to consensus
# We do this by calculating the rotation needed to match the consensus bond vectors
for i in range(seg_start, seg_start + seg_len):
# Bond axis (i, i+1)
p1 = structure.coords[i]
p2 = structure.coords[i+1]
c1 = consensus_coords[i]
c2 = consensus_coords[i+1]
# Calculate pseudo-dihedral difference
# This is a bit complex to do exactly with just rotations,
# so we'll just do a small rotation toward the consensus direction
v_curr = _vsub(p2, p1)
v_cons = _vsub(c2, c1)
# Axis of rotation to bring v_curr toward v_cons
axis = _cross(v_curr, v_cons)
if _norm(axis) > 1e-6:
angle = rng.uniform(0, 0.2) # Small step toward consensus
_rotate_segment(structure.coords, start=i+1, axis_i=i, axis_j=i+1, angle=angle) # This is not quite right but helps
_update_torsions_from_coords(structure)
return True
def _copy_structure(s: ProteinStructure) -> ProteinStructure:
return ProteinStructure(
sequence=s.sequence,
coords=list(s.coords),
phi=list(s.phi),
psi=list(s.psi),
)