Spaces:
Sleeping
Sleeping
| """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) | |
| class AminoAcid: | |
| code: str | |
| def charge(self) -> float: | |
| return RESIDUE_CHARGE.get(self.code, 0.0) | |
| def hydrophobicity(self) -> float: | |
| return HYDROPHOBICITY.get(self.code, 0.0) | |
| 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, | |
| } | |
| 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), | |
| ) | |