{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a443d433",
   "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-senescence-tipping-point.ipynb)\n\n",
    "# When Cleanup Loses: the Senescent-Cell Tipping Point 🧟‍♂️\n",
    "### A hands-on companion to Chapter 3\n",
    "\n",
    "In the book you met **senescent cells** — \"zombie\" cells that stop dividing but won't die, are produced throughout life, and are cleared by your immune system… until, in old age, they pile up and poison their neighbors (the SASP).\n",
    "\n",
    "Here's the question this notebook answers by simulation: **is that pile-up gradual, or is there a sudden threshold?**\n",
    "\n",
    "You'll discover that the *simple* model can't produce a cliff — but adding **one real fact** (the immune system has a maximum clearance speed) creates a genuine **tipping point**. That's the difference between a body that ages smoothly and one that seems fine for decades and then falls off an edge.\n",
    "\n",
    "No setup. Runs in Colab. Press ▶ on each cell, top to bottom."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dfb54fb2",
   "metadata": {},
   "source": [
    "## Step 0 — Tools, and a tiny time-stepper\n",
    "\n",
    "To \"simulate\" how the number of zombie cells changes over time, we just step forward in tiny time increments: *new amount = old amount + (rate of change) × (small time step)*. That's all the function below does."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "495fb702",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "def simulate(rate_of_change, N0, T=120.0, dt=0.02, **params):\n",
    "    \"\"\"Step the zombie-cell count N forward in time using its rate of change dN/dt.\"\"\"\n",
    "    steps = int(T/dt)\n",
    "    t = np.linspace(0, T, steps+1)\n",
    "    N = np.empty(steps+1); N[0] = N0\n",
    "    for i in range(steps):\n",
    "        N[i+1] = max(N[i] + dt*rate_of_change(N[i], **params), 0.0)   # cells can't go negative\n",
    "    return t, N\n",
    "\n",
    "print(\"Ready.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "461e86d1",
   "metadata": {},
   "source": [
    "## Part 1 — The simple model (and why it has no cliff)\n",
    "\n",
    "The simplest guess: zombie cells are **produced** at a steady rate `P`, and **cleared** at a rate proportional to how many there are, `c·N` (twice as many zombies → cleared twice as fast, with no limit).\n",
    "\n",
    "$$\\frac{dN}{dt} = P - cN$$\n",
    "\n",
    "Let's run it for a few production rates and see where the zombie count settles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d282ef19",
   "metadata": {},
   "outputs": [],
   "source": [
    "simple = lambda N, P, c: P - c*N        # production minus proportional clearance\n",
    "\n",
    "plt.figure(figsize=(6,4))\n",
    "for P in [0.3, 0.6, 1.2, 2.4]:\n",
    "    t, N = simulate(simple, N0=0, P=P, c=1.0)\n",
    "    plt.plot(t, N, label=f\"production P = {P}\")\n",
    "plt.xlabel(\"time\"); plt.ylabel(\"zombie-cell burden  N\")\n",
    "plt.title(\"Simple model: every production rate settles to a calm plateau\")\n",
    "plt.legend(); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2c61602",
   "metadata": {},
   "source": [
    "Set the rate of change to zero (`P − cN = 0`) and the plateau is **N\\* = P/c**. Double the production, double the plateau. Smooth, proportional, **no threshold anywhere.**\n",
    "\n",
    "But that's not how bodies behave — senescent cells stay low for decades and then take off. So the simple model is missing something real."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26e3714c",
   "metadata": {},
   "source": [
    "## Part 2 — One real fact: clearance has a ceiling\n",
    "\n",
    "Your immune system **cannot clear zombie cells infinitely fast.** Past a point, it's working flat out. We capture that by replacing `c·N` with a **saturating** clearance term:\n",
    "\n",
    "$$\\frac{dN}{dt} = P - \\frac{cN}{K+N}$$\n",
    "\n",
    "- At low burden, clearance rises with `N` (the more zombies, the more the immune system clears).\n",
    "- At high burden, clearance **maxes out at `c`** — the immune system's top speed.\n",
    "- `c` = maximum clearance capacity. `K` = the burden at which it's running at half-speed.\n",
    "\n",
    "Now watch what happens for production rates **below** and **above** that top speed `c = 1`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd3767d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "saturating = lambda N, P, c, K: P - c*N/(K+N)    # production minus capped clearance\n",
    "\n",
    "plt.figure(figsize=(6,4))\n",
    "for P in [0.3, 0.6, 0.9, 1.1, 1.4]:\n",
    "    t, N = simulate(saturating, N0=0, T=120, P=P, c=1.0, K=1.0)\n",
    "    runaway = \" (runaway!)\" if P >= 1.0 else \"\"\n",
    "    plt.plot(t, N, label=f\"P = {P}{runaway}\")\n",
    "plt.axhline(0, color=\"k\", lw=0.5)\n",
    "plt.xlabel(\"time\"); plt.ylabel(\"zombie-cell burden  N\")\n",
    "plt.title(\"Saturating clearance: below capacity -> plateau,  above -> runaway\")\n",
    "plt.legend(); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbc24f98",
   "metadata": {},
   "source": [
    "Setting the rate of change to zero now gives **N\\* = PK / (c − P)** — but *only when `P < c`*. As production `P` creeps toward the immune system's top speed `c`, that plateau **blows up**. And once `P ≥ c`, there is **no steady state at all**: production has outrun the fastest the immune system can possibly clear, so zombie cells accumulate **without limit.**\n",
    "\n",
    "There's your tipping point: **P = c.** Below it, a calm manageable level. Above it, runaway. Same equation — one real ceiling — and suddenly biology has a cliff."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7bf62bfe",
   "metadata": {},
   "source": [
    "## Part 3 — Draw the cliff\n",
    "\n",
    "Let's plot the resting burden **N\\* = PK/(c−P)** as production `P` sweeps from low up toward capacity `c`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "df132dab",
   "metadata": {},
   "outputs": [],
   "source": [
    "c, K = 1.0, 1.0\n",
    "P = np.linspace(0.01, 0.97, 300)        # production, up to just below capacity\n",
    "N_star = P*K/(c - P)                     # the resting burden\n",
    "\n",
    "plt.figure(figsize=(6,4))\n",
    "plt.plot(P, N_star, lw=2, color=\"#264653\")\n",
    "plt.axvline(c, color=\"#e76f51\", ls=\"--\", lw=2, label=f\"capacity  P = c = {c}\")\n",
    "plt.axvspan(c, 1.3, color=\"#e76f51\", alpha=0.12)\n",
    "plt.text(1.0, max(N_star)*0.6, \"RUNAWAY\\n(no steady state)\", color=\"#e76f51\", ha=\"left\")\n",
    "plt.xlim(0, 1.3); plt.ylim(0, max(N_star))\n",
    "plt.xlabel(\"production rate  P\"); plt.ylabel(\"resting zombie burden  N*\")\n",
    "plt.title(\"A threshold, not a slope\")\n",
    "plt.legend(); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "226128e5",
   "metadata": {},
   "source": [
    "Almost flat and harmless across most of the range — then, as production nears the immune system's top speed, the burden rockets toward infinity. Cross the line and there is no plateau to settle into at all. **This is the shape the simple `P − cN` model could never make.**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39a9276d",
   "metadata": {},
   "source": [
    "## Part 4 — Why *you* age: production rises *and* clearance falls\n",
    "\n",
    "Now the lifetime story. In youth, production `P` is low and clearance capacity `c` is high — you sit far on the safe side of the cliff, with almost no zombie burden. With age, **two** things drift the wrong way at once:\n",
    "\n",
    "- production `P` **rises** (more damage, more cells turning senescent), and\n",
    "- clearance capacity `c` **falls** (your immune system itself ages — *immunosenescence*).\n",
    "\n",
    "So the gap between them closes, you cross `P = c`, and the burden takes off. Let's simulate a whole life and watch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19325798",
   "metadata": {},
   "outputs": [],
   "source": [
    "age = np.linspace(0, 90, 9001); dt = age[1]-age[0]\n",
    "K = 0.5\n",
    "P_age = 0.25 + 1.15*(age/90)**3          # production climbs with age\n",
    "c_age = 1.05 - 0.45*(age/90)             # clearance capacity falls with age\n",
    "\n",
    "N = np.empty_like(age); N[0] = 0.3\n",
    "for i in range(len(age)-1):\n",
    "    N[i+1] = max(N[i] + dt*(P_age[i] - c_age[i]*N[i]/(K+N[i])), 0.0)\n",
    "\n",
    "cross = age[np.argmax(P_age >= c_age)]   # age where production meets falling capacity\n",
    "\n",
    "fig, ax = plt.subplots(1, 2, figsize=(11,4))\n",
    "ax[0].plot(age, P_age, label=\"production  P\"); ax[0].plot(age, c_age, label=\"clearance capacity  c\")\n",
    "ax[0].axvline(cross, color=\"gray\", ls=\":\"); ax[0].set_xlabel(\"age (years)\"); ax[0].set_ylabel(\"rate\")\n",
    "ax[0].set_title(\"The gap closes with age\"); ax[0].legend()\n",
    "ax[1].plot(age, N, color=\"#9d0208\", lw=2)\n",
    "ax[1].axvline(cross, color=\"gray\", ls=\":\"); ax[1].text(cross+1, max(N)*0.5, f\"cross P=c\\n~age {cross:.0f}\")\n",
    "ax[1].set_xlabel(\"age (years)\"); ax[1].set_ylabel(\"zombie-cell burden  N\")\n",
    "ax[1].set_title(\"Calm for decades, then a cliff\")\n",
    "plt.tight_layout(); plt.show()\n",
    "print(f\"Burden at 40: {N[np.argmin(abs(age-40))]:.2f}   at 90: {N[-1]:.2f}   (production crosses capacity ~age {cross:.0f})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41e59ea1",
   "metadata": {},
   "source": [
    "The burden barely moves for the first fifty years, then — as production overtakes the aging immune system — it lifts off. The \"sudden\" appearance of senescent cells in old age isn't the cells suddenly appearing. It's a **threshold being crossed.** This is the book's *maintenance-loses-to-damage* theme with a sharp edge."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd1edc2c",
   "metadata": {},
   "source": [
    "## Part 5 — Senolytics, and a knob to play with\n",
    "\n",
    "**Senolytics** are drugs that *clear* senescent cells — in our model, they knock the burden `N` down. Let's give one at a chosen age and see what happens afterward. Try changing the values and re-running."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ded537c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "TREAT_AGE      = 75     # when to give the senolytic  (try 55 vs 80)\n",
    "CLEAR_FRACTION = 0.80   # fraction of zombie cells it removes\n",
    "\n",
    "N = np.empty_like(age); N[0] = 0.3\n",
    "for i in range(len(age)-1):\n",
    "    n = N[i]\n",
    "    if abs(age[i] - TREAT_AGE) < dt/2:          # the treatment moment\n",
    "        n = n*(1 - CLEAR_FRACTION)\n",
    "    N[i+1] = max(n + dt*(P_age[i] - c_age[i]*n/(K+n)), 0.0)\n",
    "\n",
    "plt.figure(figsize=(6.5,4))\n",
    "plt.plot(age, N, color=\"#9d0208\", lw=2)\n",
    "plt.axvline(TREAT_AGE, color=\"#2a9d8f\", ls=\"--\", lw=2, label=f\"senolytic at age {TREAT_AGE}\")\n",
    "plt.xlabel(\"age (years)\"); plt.ylabel(\"zombie-cell burden  N\")\n",
    "plt.title(\"A senolytic knocks the burden down - but does it last?\")\n",
    "plt.legend(); plt.tight_layout(); plt.show()\n",
    "print(f\"Right after treatment the burden drops, then climbs back, because production still exceeds\")\n",
    "print(f\"the aged immune system's capacity. The drug treats the symptom, not the imbalance behind it.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bce318b",
   "metadata": {},
   "source": [
    "Notice the lesson the simulation hands you for free: a senolytic **lowers the burden but doesn't move the threshold.** If you're already past `P = c`, the cells grow right back, and you'd need repeated doses — unless the treatment *also* lowered production or restored clearance. That's exactly the kind of honesty Chapter 3 asks for: clearing zombie cells in a model (or a mouse) is promising, **not** a proven human therapy.\n",
    "\n",
    "### Go further (for the reader who finished hungry)\n",
    "\n",
    "- **Sweep the knobs.** Change `P_age`, `c_age`, and `K`. How early can you push the crossing? Can you keep someone under the threshold for a whole lifetime?\n",
    "- **Add SASP feedback.** In reality, senescent cells secrete signals that turn *other* cells senescent — so production should grow with the current burden: `P = P0 + b*N`. That positive feedback can create **two** stable states (healthy-low and diseased-high) with a tipping point between them — a sharper, scarier cliff. Try it.\n",
    "- **Connect it to the book.** This non-linear, threshold behavior is the same *\"biology runs on curves, not lines\"* idea you met as hormesis in Chapter 5 — here with the highest stakes.\n",
    "\n",
    "---\n",
    "*Companion notebook to* **Chapter 3 — Cellular Senescence**, *from the \"AI for Drug Discovery for Kids\" aging & longevity volume. A teaching model: the equations are real, the numbers are illustrative.*"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}