/- BindingSite.BindingSiteEntropy — Information entropy for protein binding sites. §1-§4: computable, Q16_16, no Float, no Real. §5: noncomputable theorems using Real — legitimate math, not IO compute path. fisher_implies_similar_druggability is BLOCKED on entropy_lipschitz axiom (research-level Pinsker-type inequality; see inline documentation). References: - Yang, Yuan, Chou 2025 (Void-X): Eq. 3 (information entropy) - Giani, Win, Conti 2025: quantum discrimination via PVGS -/ import BindingSite.BindingSiteHachimoji import Mathlib.Data.Real.Basic import Mathlib.Topology.Basic import Mathlib.Analysis.SpecialFunctions.Log.Basic import Mathlib.Analysis.Complex.ExponentialBounds namespace BindingSite open SilverSight.FixedPoint -- ═══════════════════════════════════════════════════════════════════════════ -- §1 Information entropy over a probability distribution (noncomputable/math) -- ═══════════════════════════════════════════════════════════════════════════ /-- Shannon entropy of a probability distribution over 50 atom types. Void-X Eq. 3: S_i = -∑_j p(a_j|context) log p(a_j|context). Noncomputable: uses ℝ and Real.log; for math section only. -/ noncomputable def siteEntropy (p : AminoAcidDistribution) : ℝ := -∑ i : Fin 50, if p.val i > 0 then p.val i * Real.log (p.val i) else 0 /-- Maximum possible entropy for 50 states (uniform distribution). S_max = log(50) ≈ 3.912. -/ noncomputable def maxEntropy50 : ℝ := Real.log 50 /-- Normalized entropy: S* = S / S_max ∈ [0, 1]. -/ noncomputable def normalizedEntropy (p : AminoAcidDistribution) : ℝ := siteEntropy p / maxEntropy50 -- ═══════════════════════════════════════════════════════════════════════════ -- §2 B-factor → entropy (computable, Q16_16) -- ═══════════════════════════════════════════════════════════════════════════ -- Re-exported from BindingSiteTypes; provided here for namespace convenience. /-- Compute average entropy for a residue from its B-factor and neighbors. Uses the linear proxy entropy = clamp(avg_bFactor, 0, 100) / 100. Monotone, deterministic, Q16_16. -/ def siteEntropyQ (bFactor : Nat) (neighborBFactors : List Nat) : Q16_16 := entropyFromBFactor bFactor neighborBFactors /-- Entropy from sequence cluster membership. Higher cluster diversity → higher entropy; higher conservation → lower entropy. Q16_16 arithmetic: diversity in [0,1], conservation in [0,1]. -/ def entropyFromCluster (clusterSize : Nat) (sequenceIdentityQ : Q16_16) : Q16_16 := -- diversity = clusterSize / maxClusterSize, clamp to [0,1] let maxCluster : Nat := 10000 let diversityQ := Q16_16.ofRawInt ((min clusterSize maxCluster : Int) * 65536 / (maxCluster : Int)) -- conservation term: (1 - sequenceIdentity) let oneMinusConserv := Q16_16.sub Q16_16.one sequenceIdentityQ Q16_16.mul diversityQ oneMinusConserv -- ═══════════════════════════════════════════════════════════════════════════ -- §3 Binding site entropy profile (computable, Q16_16) -- ═══════════════════════════════════════════════════════════════════════════ /-- Compute the full entropy profile of a binding site from raw PDB residue data. Input: (residue_type, modification, b_factor_nat, neighbor_b_factors_nat) Output: (AminoAcidToken × Q16_16 × BindingSiteState) list -/ def bindingSiteEntropyProfile (residues : List (String × String × Nat × List Nat)) : List (AminoAcidToken × Q16_16 × BindingSiteState) := residues.map fun (resType, mod, bFactor, neighborBFs) => let token := residueToToken resType mod let entropy := entropyFromBFactor bFactor neighborBFs let state := entropyToHachimoji entropy false (mod == "ZN" || mod == "CA") (token, entropy, state) /-- Average entropy of a classified site profile (Q16_16). -/ def averageSiteEntropyQ (profile : List (AminoAcidToken × Q16_16 × BindingSiteState)) : Q16_16 := q16Mean (profile.map (·.2.1)) /-- Bindability score B* ∈ [0, 100] in Q16_16. B* = 100 × (1 - (avgEntropy - globalMin) / (globalMax - globalMin)). High B* ↔ low entropy relative to protein surface ↔ ordered pocket. -/ def bindabilityScore (profile : List (AminoAcidToken × Q16_16 × BindingSiteState)) (globalMin globalMax : Q16_16) : Q16_16 := let avg := averageSiteEntropyQ profile let range := Q16_16.sub globalMax globalMin if range.val ≤ 0 then Q16_16.ofNat 50 -- degenerate: return mid-score else let normalized := Q16_16.div (Q16_16.sub avg globalMin) range Q16_16.mul (Q16_16.ofNat 100) (Q16_16.sub Q16_16.one normalized) -- ═══════════════════════════════════════════════════════════════════════════ -- §4 Sidon address from entropy profile (computable, Q16_16/Nat) -- ═══════════════════════════════════════════════════════════════════════════ /-- Map a BindingSiteState to its Sidon index ∈ {0,…,7}. -/ def stateToSidonIdx : BindingSiteState → Nat | .Phi => 0 | .Lambda => 1 | .Rho => 2 | .Kappa => 3 | .Omega => 4 | .Sigma => 5 | .Pi => 6 | .Zeta => 7 /-- Compute the Sidon address of a binding site from its entropy profile. The 8 dominant entropy values map to Sidon powers {2⁰,…,2⁷}, weighted by entropy magnitude (clamped to [0,16]). -/ def entropyToSidonAddress (profile : List (AminoAcidToken × Q16_16 × BindingSiteState)) : List Nat := profile.filterMap fun (_, entropy, state) => let idx := stateToSidonIdx state -- entropy.val ∈ [0, 65536]; scale to [0, 16] let scale := (entropy.val * 16 / 65536).toNat some (Nat.pow 2 idx * scale) -- ═══════════════════════════════════════════════════════════════════════════ -- §5 Fisher metric on binding site manifold (noncomputable math) -- ═══════════════════════════════════════════════════════════════════════════ /-- Approximate Fisher-Rao distance via Bhattacharyya coefficient. d_FR(p,q) ≈ sqrt(2 * log(1 / Σ_i sqrt(p_i * q_i))). Noncomputable: uses Real arithmetic. -/ noncomputable def fisherRaoApprox (p q : AminoAcidDistribution) : ℝ := Real.sqrt (2 * Real.log (1 / ∑ i : Fin 50, Real.sqrt (p.val i * q.val i))) /-- The binding site manifold: probability distributions over residue tokens with the Fisher metric. Geodesics are evolutionarily optimal paths. -/ structure BindingSiteManifold where distribution : AminoAcidDistribution metric : Fin 50 → Fin 50 → ℝ := fisherMetric50 distribution entropy : ℝ := siteEntropy distribution -- ───────────────────────────────────────────────────────────────────────── -- §5.1 Entropy Lipschitz axiom (research-level; unblocks §5.2) -- ───────────────────────────────────────────────────────────────────────── /-- Shannon entropy is Lipschitz w.r.t. Fisher-Rao distance, constant L = sqrt(2·log 50). Pinsker-type inequality; research-level analytical result. Informally: nearby distributions on the statistical manifold have nearby entropies. HONESTY CLASS: CITED JUSTIFICATION: Pinsker's inequality (standard information theory result) -/ axiom entropy_lipschitz (p q : AminoAcidDistribution) : |siteEntropy p - siteEntropy q| ≤ Real.sqrt (2 * maxEntropy50) * fisherRaoApprox p q -- ───────────────────────────────────────────────────────────────────────── -- §5.2 Classification stability theorem -- ───────────────────────────────────────────────────────────────────────── /-- Nearby binding sites (Fisher-Rao distance < 0.1) have compatible druggability. Uses NORMALIZED entropy N = S/S_max ∈ [0,1] so Q16_16-derived thresholds apply: T₁ = 39321/65536 ≈ 0.600 (druggable-pocket boundary) T₂ = 26214/65536 ≈ 0.400 (moderate-entropy floor) Proof sketch: 1. log(50) > 2 (since exp(2) < 9 < 50) 2. sqrt(2/M) < 1 (since M > 2) 3. |N(p) - N(q)| ≤ sqrt(2/M)·d_FR < 1·0.1 = 0.1 4. threshold gap T₁ - T₂ = 13107/65536 ≈ 0.200 > 0.1 5. If sites straddle T₁, the lower one is still > T₁ - 0.1 > T₂ -/ theorem fisher_implies_similar_druggability (p q : AminoAcidDistribution) (h : fisherRaoApprox p q < 0.1) : let s1 := if normalizedEntropy p ≥ 39321 / 65536 then true else false let s2 := if normalizedEntropy q ≥ 39321 / 65536 then true else false s1 = s2 ∨ (normalizedEntropy p > 26214 / 65536 ∧ normalizedEntropy q > 26214 / 65536) := by -- 1. log(50) > 2 ← exp(2) < 9 < 50 have hM2 : (2 : ℝ) < maxEntropy50 := by show (2 : ℝ) < Real.log 50 have h1 : Real.exp 1 < 3 := Real.exp_one_lt_three have h2 : Real.exp 2 = Real.exp 1 * Real.exp 1 := by rw [show (2 : ℝ) = 1 + 1 from by norm_num, Real.exp_add] have hexp2 : Real.exp 2 < 50 := by nlinarith [Real.exp_pos (1 : ℝ)] calc (2 : ℝ) = Real.log (Real.exp 2) := (Real.log_exp 2).symm _ < Real.log 50 := Real.log_lt_log (Real.exp_pos 2) hexp2 have hM : (0 : ℝ) < maxEntropy50 := by linarith -- 2. sqrt(2·M) ≤ M ← 2·M ≤ M² ← M ≥ 2 have hsqrt_le : Real.sqrt (2 * maxEntropy50) ≤ maxEntropy50 := by calc Real.sqrt (2 * maxEntropy50) ≤ Real.sqrt (maxEntropy50 ^ 2) := Real.sqrt_le_sqrt (by nlinarith) _ = maxEntropy50 := Real.sqrt_sq hM.le -- 3. |N(p) - N(q)| < 1/10 have hNL := entropy_lipschitz p q have hbound : |siteEntropy p - siteEntropy q| < 1 / 10 * maxEntropy50 := calc |siteEntropy p - siteEntropy q| ≤ Real.sqrt (2 * maxEntropy50) * fisherRaoApprox p q := hNL _ ≤ maxEntropy50 * fisherRaoApprox p q := mul_le_mul_of_nonneg_right hsqrt_le (Real.sqrt_nonneg _) _ < maxEntropy50 * (1 / 10) := mul_lt_mul_of_pos_left (by linarith) hM _ = 1 / 10 * maxEntropy50 := by ring have hNdiff : |normalizedEntropy p - normalizedEntropy q| < 1 / 10 := by have heq : normalizedEntropy p - normalizedEntropy q = (siteEntropy p - siteEntropy q) / maxEntropy50 := by simp [normalizedEntropy, sub_div] -- |N|·M = |S| (by dividing), then nlinarith from |S| < (1/10)·M have hmul : |normalizedEntropy p - normalizedEntropy q| * maxEntropy50 = |siteEntropy p - siteEntropy q| := by rw [heq, abs_div, abs_of_pos hM]; field_simp [hM.ne'] nlinarith [abs_nonneg (normalizedEntropy p - normalizedEntropy q)] -- 4. Case analysis show (if normalizedEntropy p ≥ 39321 / 65536 then true else false) = (if normalizedEntropy q ≥ 39321 / 65536 then true else false) ∨ (normalizedEntropy p > 26214 / 65536 ∧ normalizedEntropy q > 26214 / 65536) split_ifs with h1 h2 · left; rfl · -- p ≥ T₁, q < T₁ → N(q) > N(p) - 0.1 ≥ T₁ - 0.1 > T₂ right have h2' : normalizedEntropy q < 39321 / 65536 := not_le.mp h2 have hnn : (0 : ℝ) ≤ normalizedEntropy p - normalizedEntropy q := by linarith have hd : normalizedEntropy p - normalizedEntropy q < 1 / 10 := by rwa [abs_of_nonneg hnn] at hNdiff constructor · linarith [show (39321 : ℝ) / 65536 > 26214 / 65536 from by norm_num] · linarith [show (39321 : ℝ) / 65536 - 1 / 10 > 26214 / 65536 from by norm_num] · -- p < T₁, q ≥ T₁ → N(p) > N(q) - 0.1 ≥ T₁ - 0.1 > T₂ right have h1' : normalizedEntropy p < 39321 / 65536 := not_le.mp h1 have hneg : normalizedEntropy p - normalizedEntropy q ≤ 0 := by linarith have habsform : |normalizedEntropy p - normalizedEntropy q| = normalizedEntropy q - normalizedEntropy p := by rw [abs_of_nonpos hneg]; ring have hd : normalizedEntropy q - normalizedEntropy p < 1 / 10 := habsform ▸ hNdiff constructor · linarith [show (39321 : ℝ) / 65536 - 1 / 10 > 26214 / 65536 from by norm_num] · linarith [show (39321 : ℝ) / 65536 > 26214 / 65536 from by norm_num] · left; rfl end BindingSite