{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0b00f2fc",
   "metadata": {},
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/MeiraDavidson/biology_aging/blob/main/notebooks/build-protein-folding-funnel.ipynb)\n\n",
    "# Why Proteins Don't Fold by Searching 🧬🌀\n",
    "### A hands-on companion to Chapter 6\n",
    "\n",
    "A protein is a chain that must find its *one* correct 3D shape to work. Chapter 6 asks the deep question: **how?** Here you'll first compute how many shapes a protein could take (a number past comprehension), see that blind trial-and-error would take longer than the universe has existed — and then watch *why* a protein folds in millionths of a second anyway.\n",
    "\n",
    "No setup. Runs in Colab. Press ▶ top to bottom."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe87db31",
   "metadata": {},
   "source": [
    "## Part 1 — Levinthal's paradox: count the shapes\n",
    "\n",
    "A protein of **N** amino acids has roughly **k** stable angles at each junction. So the number of possible shapes is **kᴺ** — multiplication run wild. Let's count for a small protein, and time how long it would take to try them all at a blistering rate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f620f968",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np, math\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "N, k = 100, 3                     # 100 residues, ~3 stable angles each\n",
    "n_shapes = k**N\n",
    "rate     = 1e13                   # try 10 trillion shapes per second (absurdly fast)\n",
    "seconds  = n_shapes / rate\n",
    "age_universe = 4.35e17            # seconds since the Big Bang\n",
    "\n",
    "print(f\"A {N}-residue protein has about {n_shapes:.1e} possible shapes.\")\n",
    "print(f\"Trying {rate:.0e} per second, searching them all takes {seconds:.1e} seconds,\")\n",
    "print(f\"which is {seconds/age_universe:.1e} times the entire age of the universe.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6993cef8",
   "metadata": {},
   "source": [
    "So a protein **cannot** find its shape by trying shapes at random — there isn't remotely enough time in the universe. Yet real proteins fold in **microseconds.** That contradiction is **Levinthal's paradox**, and its resolution is the whole point of this chapter."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18ca31db",
   "metadata": {},
   "source": [
    "## Part 2 — Two landscapes: a golf course vs a funnel\n",
    "\n",
    "Picture the protein rolling on an *energy landscape*, always tending downhill toward lower energy. Levinthal imagined a flat **\"golf course\"**: featureless everywhere, with the native shape a single hole you can only find by blind luck. The modern picture is a **\"folding funnel\"**: the landscape slopes steadily downward toward the native shape, *guiding* the protein in.\n",
    "\n",
    "Let's simulate a search on each — in several dimensions, like a real protein — and watch the distance to the correct shape over time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce85cc5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def fold_search(kind, D=8, steps=8000, T=0.04, step=0.08, seed=0):\n",
    "    \"\"\"Roll on an energy landscape; track distance to the native shape (the origin).\"\"\"\n",
    "    rng = np.random.default_rng(seed)\n",
    "    x = rng.uniform(-1, 1, D)                       # a random starting shape\n",
    "    distance = [np.linalg.norm(x)]\n",
    "    if kind == \"funnel\":\n",
    "        energy = lambda v: np.sum(np.abs(v))        # slopes downhill toward origin\n",
    "    else:  # golf course: flat everywhere, tiny hole only at the native shape\n",
    "        energy = lambda v: -5.0 if np.linalg.norm(v) < 0.15 else 0.0\n",
    "    for _ in range(steps):\n",
    "        trial = np.clip(x + rng.uniform(-step, step, D), -1, 1)\n",
    "        if rng.random() < math.exp(-(energy(trial)-energy(x))/T):  # accept downhill (mostly)\n",
    "            x = trial\n",
    "        distance.append(np.linalg.norm(x))\n",
    "        if np.linalg.norm(x) < 0.08:                # folded!\n",
    "            break\n",
    "    return np.array(distance)\n",
    "\n",
    "funnel = fold_search(\"funnel\")\n",
    "golf   = fold_search(\"golf\")\n",
    "\n",
    "print(f\"Funnel landscape: FOLDED after {len(funnel)} steps.\")\n",
    "print(f\"Golf-course landscape: after {len(golf)} steps, still lost (closest it ever got: {golf.min():.2f}).\")\n",
    "\n",
    "plt.figure(figsize=(7,4.5))\n",
    "plt.plot(golf,   color=\"#e76f51\", label=\"golf course (flat - blind search)\")\n",
    "plt.plot(funnel, color=\"#2a9d8f\", label=\"funnel (guided downhill)\")\n",
    "plt.axhline(0.08, color=\"k\", ls=\":\", lw=1, label=\"folded\")\n",
    "plt.xlabel(\"search steps\"); plt.ylabel(\"distance to the correct shape\")\n",
    "plt.title(\"Same protein, two landscapes\"); plt.legend(); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dcd9f1e8",
   "metadata": {},
   "source": [
    "The funnel folds in about a thousand steps. The golf-course search never even gets close — and scaled up to a real protein's dimensions, it would take the astronomically long time you computed in Part 1. **The shape of the landscape is everything.** Evolution didn't just design protein *sequences*; it designed sequences whose landscapes are *funnels*, so folding is fast and reliable.\n",
    "\n",
    "## Part 3 — The connection to AI and to aging\n",
    "\n",
    "This is the same *\"search an impossibly large space\"* problem you met with the epigenetic clock (Chapter 2) and that sits under AI drug discovery — and it's exactly why **AlphaFold** was such a leap: it doesn't *search* shapes at all, it **predicts the bottom of the funnel directly**. But keep Chapter 6's caution in view: predicting the *right* fold is a different problem from the *wrong* folds — the misfolding and aggregation that drive Alzheimer's and Parkinson's — which is the part aging actually turns on.\n",
    "\n",
    "### Go further\n",
    "- Change `N` and `k` in Part 1. How big does the protein have to be before the search beats the age of the universe?\n",
    "- Add *ruggedness* to the funnel (a few local dips) to model **misfolding traps**, and watch some runs get stuck.\n",
    "- Raise the temperature `T`. Too cold and it freezes; too hot and it never settles — another inverted-U, like Chapter 4's hormesis."
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}