{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8ac75d85",
   "metadata": {},
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/MeiraDavidson/math_module2/blob/main/notebooks/ch10_integral.ipynb)\n\n",
    "# Chapter 10 — Companion Notebook\n",
    "## Adding Up the Infinite\n",
    "*how to measure the area under a curve — the second great engine of calculus*\n",
    "\n",
    "Four live pictures of the integral's two faces: dial up the rectangles of a **Riemann sum** and watch the staircase melt onto the curve, check an **antiderivative** by differentiating it back, run the **chain rule backward** as a substitution, and sweep a movable right edge to watch the **accumulated area** quietly draw the antiderivative you meet in Chapter 11.\n",
    "\n",
    "> **How to use this notebook.** Run every cell from the top (Shift+Enter). Each widget below is live — drag the sliders and watch the picture respond. Requires `ipywidgets` and `matplotlib`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "36b974fb",
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib import animation\n",
    "from ipywidgets import (interact, interactive, fixed, Dropdown, Checkbox,\n",
    "                        Button, Output, VBox, HBox,\n",
    "                        FloatSlider, IntSlider, FloatLogSlider)\n",
    "from IPython.display import HTML, display, Markdown\n",
    "\n",
    "# Book 2 palette — matches the printed figures in figures/png/\n",
    "BLUE=\"#1f4e79\"; RED=\"#c0392b\"; GREEN=\"#2e8b57\"; ORANGE=\"#e08e0b\"\n",
    "PURPLE=\"#6c3483\"; TEAL=\"#1b9e9e\"; GRAY=\"#7f8c8d\"; DARK=\"#2c3e50\"\n",
    "SHADE=\"#9fc5e8\"; SHADE2=\"#f4b6ad\"; SHADEG=\"#a9dfbf\"; LIGHT=\"#d6dbdf\"\n",
    "\n",
    "plt.rcParams.update({\n",
    "    \"figure.figsize\": (7.2, 4.5), \"figure.dpi\": 96,\n",
    "    \"font.family\": \"serif\", \"mathtext.fontset\": \"cm\", \"font.size\": 12,\n",
    "    \"axes.spines.top\": False, \"axes.spines.right\": False,\n",
    "    \"axes.grid\": True, \"grid.color\": LIGHT, \"grid.linewidth\": 0.7,\n",
    "    \"lines.linewidth\": 2.2, \"lines.solid_capstyle\": \"round\",\n",
    "})\n",
    "\n",
    "def axes(ax=None, title=None):\n",
    "    '''A clean axes with a light grid; returns the axes.'''\n",
    "    if ax is None:\n",
    "        _, ax = plt.subplots()\n",
    "    if title:\n",
    "        ax.set_title(title, color=DARK)\n",
    "    return ax\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "481097e5",
   "metadata": {},
   "source": [
    "## 1. Riemann sums: rectangles closing in on the area\n",
    "\n",
    "We cannot measure the area under a curve directly, so we *approximate* it with rectangles and then take a limit. Chop $[a,b]$ into $n$ equal slices of width $\\Delta x = \\frac{b-a}{n}$, stand a rectangle on each whose height is $f$ at a **sample point**, and add them up:\n",
    "\n",
    "$$\\int_a^b f(x)\\,dx \\;\\approx\\; \\sum_{i=1}^{n} f(x_i)\\,\\Delta x.$$\n",
    "\n",
    "Pick a function, drag $n$ upward, and switch the sample point between **left**, **right**, and **midpoint**. The blue staircase hugs the curve more tightly with every new rectangle, and the **error** against the true integral (computed numerically on a fine grid) shrinks toward zero — *(Figures 10.2 and 10.3)*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c81e6cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# (f, [a, b], a label for the exact value) for each preset.\n",
    "_RIEMANN = {\n",
    "    'f(x) = x^2  on [0, 2]':   (lambda x: x**2,        0.0, 2.0),\n",
    "    'f(x) = x^3  on [0, 1]':   (lambda x: x**3,        0.0, 1.0),\n",
    "    'f(x) = sin(x)  on [0, pi]': (lambda x: np.sin(x),  0.0, np.pi),\n",
    "    'f(x) = sqrt(x)  on [0, 4]': (lambda x: np.sqrt(x), 0.0, 4.0),\n",
    "    'f(x) = 1/x  on [1, 3]':   (lambda x: 1.0/x,       1.0, 3.0),\n",
    "}\n",
    "\n",
    "def _sample_x(a, b, n, where):\n",
    "    '''Left/right/midpoint sample points for the n slices of [a,b].'''\n",
    "    edges = np.linspace(a, b, n + 1)\n",
    "    if where == 'left':\n",
    "        return edges[:-1]\n",
    "    if where == 'right':\n",
    "        return edges[1:]\n",
    "    return 0.5 * (edges[:-1] + edges[1:])   # midpoint\n",
    "\n",
    "def _riemann(curve='f(x) = x^2  on [0, 2]', n=4, sample='left'):\n",
    "    f, a, b = _RIEMANN[curve]\n",
    "    dx = (b - a) / n\n",
    "    xs_rect = _sample_x(a, b, n, sample)\n",
    "    heights = f(xs_rect)\n",
    "    approx = float(np.sum(heights) * dx)\n",
    "    # True integral: trapezoid rule on a fine grid.\n",
    "    fine = np.linspace(a, b, 4001)\n",
    "    exact = float(np.trapz(f(fine), fine))\n",
    "    err = approx - exact\n",
    "\n",
    "    fig, ax = plt.subplots()\n",
    "    left_edges = np.linspace(a, b, n + 1)[:-1]\n",
    "    ax.bar(left_edges, heights, width=dx, align='edge',\n",
    "           facecolor=SHADE, edgecolor=BLUE, linewidth=1.1,\n",
    "           alpha=0.85, zorder=1)\n",
    "    ax.plot(fine, f(fine), color=BLUE, lw=2.6, zorder=3,\n",
    "            label='$f(x)$')\n",
    "    ax.scatter(xs_rect, heights, s=22, color=RED, zorder=4,\n",
    "               label=f'{sample} sample points')\n",
    "    ax.axhline(0, color=GRAY, lw=0.8)\n",
    "    ax.set_xlim(a - 0.05 * (b - a), b + 0.05 * (b - a))\n",
    "    ax.set_xlabel('$x$'); ax.set_ylabel('$y$')\n",
    "    ax.set_title(f'{sample.capitalize()} Riemann sum,  $n = {n}$',\n",
    "                 color=DARK)\n",
    "    ax.legend(loc='best', framealpha=0.9, fontsize=10)\n",
    "    plt.show()\n",
    "    display(Markdown(\n",
    "        f'**Riemann sum** $\\\\approx {approx:.5f}$ &nbsp;&nbsp; '\n",
    "        f'**true integral** $\\\\approx {exact:.5f}$ &nbsp;&nbsp; '\n",
    "        f'**error** $= {err:+.5f}$'))\n",
    "\n",
    "interact(_riemann,\n",
    "         curve=Dropdown(options=list(_RIEMANN),\n",
    "                        description='f'),\n",
    "         n=IntSlider(value=4, min=1, max=120, step=1,\n",
    "                     description='n rects'),\n",
    "         sample=Dropdown(options=['left', 'right', 'midpoint'],\n",
    "                         value='left', description='sample'));\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3602d81",
   "metadata": {},
   "source": [
    "## 2. Antiderivative checker: differentiate it back\n",
    "\n",
    "An **antiderivative** $F$ of $f$ is a function whose derivative *is* $f$. The only honest way to check a proposed $F$ is to differentiate it and see whether you land back on $f$ — substitution's own proofreader.\n",
    "\n",
    "Each preset offers a function $f$ (blue) and a *proposed* antiderivative $F$. We differentiate $F$ **numerically** and overlay $F'$ (red dashes). If the red curve lies on top of the blue one, the proposal is right; if it drifts off, it is wrong. Tick **reveal** for the verdict and the worked answer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a6b13b80",
   "metadata": {},
   "outputs": [],
   "source": [
    "# (f, proposed F, is_correct, worked LaTeX) per preset.\n",
    "_ANTI = {\n",
    "    'f = 2x,  F = x^2':\n",
    "        (lambda x: 2*x, lambda x: x**2, True,\n",
    "         r\"$\\frac{d}{dx}\\,x^2 = 2x$ \\u2713  correct.\"),\n",
    "    'f = cos x,  F = sin x':\n",
    "        (lambda x: np.cos(x), lambda x: np.sin(x), True,\n",
    "         r\"$\\frac{d}{dx}\\,\\sin x = \\cos x$ \\u2713  correct.\"),\n",
    "    'f = sin x,  F = cos x':\n",
    "        (lambda x: np.sin(x), lambda x: np.cos(x), False,\n",
    "         r\"$\\frac{d}{dx}\\,\\cos x = -\\sin x \\ne \\sin x$. \"\n",
    "         r\"Wrong sign \\u2014 the antiderivative of $\\sin x$ is \"\n",
    "         r\"$-\\cos x + C$.\"),\n",
    "    'f = x^2,  F = x^3/3':\n",
    "        (lambda x: x**2, lambda x: x**3/3, True,\n",
    "         r\"$\\frac{d}{dx}\\,\\frac{x^3}{3} = x^2$ \\u2713  correct \"\n",
    "         r\"(reverse power rule).\"),\n",
    "    'f = x^2,  F = x^3':\n",
    "        (lambda x: x**2, lambda x: x**3, False,\n",
    "         r\"$\\frac{d}{dx}\\,x^3 = 3x^2 \\ne x^2$. Forgot to divide \"\n",
    "         r\"by the new power: it should be $\\frac{x^3}{3} + C$.\"),\n",
    "    'f = e^x,  F = e^x':\n",
    "        (lambda x: np.exp(x), lambda x: np.exp(x), True,\n",
    "         r\"$\\frac{d}{dx}\\,e^x = e^x$ \\u2713  its own antiderivative.\"),\n",
    "}\n",
    "\n",
    "def _num_deriv(F, x, h=1e-5):\n",
    "    '''Symmetric difference quotient \\u2014 a numerical derivative.'''\n",
    "    return (F(x + h) - F(x - h)) / (2 * h)\n",
    "\n",
    "def _checker(which='f = 2x,  F = x^2', reveal=False):\n",
    "    f, F, ok, work = _ANTI[which]\n",
    "    xs = np.linspace(-2.2, 2.2, 600)\n",
    "    fig, ax = plt.subplots()\n",
    "    ax.plot(xs, f(xs), color=BLUE, lw=2.8, label='$f(x)$', zorder=2)\n",
    "    ax.plot(xs, _num_deriv(F, xs), color=RED, lw=2.0, ls='--',\n",
    "            label=\"$F'(x)$  (computed)\", zorder=3)\n",
    "    ax.axhline(0, color=GRAY, lw=0.8)\n",
    "    ax.set_xlim(-2.2, 2.2)\n",
    "    ax.set_ylim(-4.5, 4.5)\n",
    "    ax.set_xlabel('$x$'); ax.set_ylabel('$y$')\n",
    "    ax.set_title('Does  $F\\\\,' + chr(39) + ' = f$ ?', color=DARK)\n",
    "    ax.legend(loc='upper left', framealpha=0.9, fontsize=10)\n",
    "    plt.show()\n",
    "    if reveal:\n",
    "        verdict = '**Correct.**' if ok else '**Not an antiderivative.**'\n",
    "        display(Markdown(verdict))\n",
    "        display(Markdown(work))\n",
    "    else:\n",
    "        display(Markdown('*Curves overlap? Then $F$ is an '\n",
    "                         'antiderivative. Tick **reveal** for the '\n",
    "                         'verdict.*'))\n",
    "\n",
    "interact(_checker,\n",
    "         which=Dropdown(options=list(_ANTI), description='proposal'),\n",
    "         reveal=Checkbox(value=False, description='reveal answer'));\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58dda8f6",
   "metadata": {},
   "source": [
    "## 3. Substitution workbench: the chain rule, run backward\n",
    "\n",
    "Substitution works when an **inside function** $u = g(x)$ has its derivative $g'(x)$ *already sitting in the integrand*. Renaming $u = g(x)$, $du = g'(x)\\,dx$ collapses the integral into one you can read off the table:\n",
    "\n",
    "$$\\int 2x\\,(x^2+1)^5\\,dx \\;=\\; \\int u^5\\,du \\;=\\; \\frac{u^6}{6} + C \\;=\\; \\frac{(x^2+1)^6}{6} + C.$$\n",
    "\n",
    "The left panel shows the integrand in $x$ with the inside $u = g(x)$ (green) and its derivative $g'(x)$ (orange) — the very factor that makes the trick legal. The right panel is the same integral seen in the new coordinate: the plain $u^5$. Sweep the **upper limit** $b$; the two shaded areas stay equal, because substitution only *renames* — it never changes the area *(Figure 10.5)*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7344e7d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Each preset: integrand h(x), inside g(x), its derivative gp(x),\n",
    "# the post-substitution integrand phi(u), and the u-power label.\n",
    "_SUBS = {\n",
    "    '2x (x^2+1)^5':\n",
    "        dict(h=lambda x: 2*x*(x**2+1)**5,\n",
    "             g=lambda x: x**2 + 1, gp=lambda x: 2*x,\n",
    "             phi=lambda u: u**5,\n",
    "             u_lbl='u = x^2 + 1', du_lbl='du = 2x\\\\,dx',\n",
    "             integ='\\\\int u^5\\\\,du = \\\\dfrac{u^6}{6} + C'),\n",
    "    '3x^2 (x^3+1)^4':\n",
    "        dict(h=lambda x: 3*x**2*(x**3+1)**4,\n",
    "             g=lambda x: x**3 + 1, gp=lambda x: 3*x**2,\n",
    "             phi=lambda u: u**4,\n",
    "             u_lbl='u = x^3 + 1', du_lbl='du = 3x^2\\\\,dx',\n",
    "             integ='\\\\int u^4\\\\,du = \\\\dfrac{u^5}{5} + C'),\n",
    "}\n",
    "\n",
    "def _subwork(which='2x (x^2+1)^5', b=1.0):\n",
    "    p = _SUBS[which]\n",
    "    h, g, gp, phi = p['h'], p['g'], p['gp'], p['phi']\n",
    "    a = 0.0\n",
    "    xs = np.linspace(a, 1.5, 500)\n",
    "    xs_fill = np.linspace(a, b, 300)\n",
    "    # In u-coordinates, x in [a, b] maps to u in [g(a), g(b)].\n",
    "    ua, ub = g(a), g(b)\n",
    "    us = np.linspace(ua, max(ub, ua + 1e-6), 500)\n",
    "    us_fill = np.linspace(ua, ub, 300)\n",
    "    # The two areas are equal by the substitution rule.\n",
    "    area_x = float(np.trapz(h(xs_fill), xs_fill))\n",
    "    area_u = float(np.trapz(phi(us_fill), us_fill))\n",
    "\n",
    "    fig, (axL, axR) = plt.subplots(1, 2, figsize=(11.0, 4.4))\n",
    "    # Left: the integrand in x, with inside g and its derivative.\n",
    "    axL.plot(xs, h(xs), color=BLUE, lw=2.6,\n",
    "             label='integrand $2x\\\\,(x^2{+}1)^5$'\n",
    "             if which.startswith('2x') else 'integrand')\n",
    "    axL.fill_between(xs_fill, h(xs_fill), color=SHADEG, alpha=0.8,\n",
    "                     zorder=1)\n",
    "    axL.plot(xs, g(xs), color=GREEN, lw=2.0, ls='-',\n",
    "             label='inside  $' + p['u_lbl'] + '$')\n",
    "    axL.plot(xs, gp(xs), color=ORANGE, lw=2.0, ls='--',\n",
    "             label=\"its derivative  $g'(x)$\")\n",
    "    axL.axvline(b, color=RED, lw=1.2, ls=':')\n",
    "    axL.set_xlim(a, 1.5)\n",
    "    axL.set_xlabel('$x$'); axL.set_ylabel('$y$')\n",
    "    axL.set_title(f'In $x$:  area to $b={b:.2f}$ is {area_x:.4f}',\n",
    "                  color=DARK)\n",
    "    axL.legend(loc='upper left', framealpha=0.9, fontsize=9)\n",
    "    # Right: the same integral in u.\n",
    "    axR.plot(us, phi(us), color=BLUE, lw=2.6,\n",
    "             label='$' + ('u^5' if which.startswith('2x') else 'u^4')\n",
    "             + '$')\n",
    "    axR.fill_between(us_fill, phi(us_fill), color=SHADEG, alpha=0.8,\n",
    "                     zorder=1)\n",
    "    axR.axvline(ub, color=RED, lw=1.2, ls=':')\n",
    "    axR.set_xlabel('$u$'); axR.set_ylabel('$y$')\n",
    "    axR.set_title(f'In $u$:  same area = {area_u:.4f}', color=DARK)\n",
    "    axR.legend(loc='upper left', framealpha=0.9, fontsize=10)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    display(Markdown(\n",
    "        f\"Let $\" + p['u_lbl'] + \"$, so $\" + p['du_lbl'] + \"$.  \"\n",
    "        f\"The integral becomes $\" + p['integ'] + \"$ \\u2014 and the \"\n",
    "        f\"two shaded areas agree to numerical precision \"\n",
    "        f\"(${area_x:.4f} = {area_u:.4f}$).\"))\n",
    "\n",
    "interact(_subwork,\n",
    "         which=Dropdown(options=list(_SUBS), description='integrand'),\n",
    "         b=FloatSlider(value=1.0, min=0.1, max=1.5, step=0.05,\n",
    "                       description='upper b'));\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51a8ca61",
   "metadata": {},
   "source": [
    "## 4. Area accumulator: watch the antiderivative draw itself\n",
    "\n",
    "Fix the left edge at $a$ and let the right edge $X$ move. The **accumulated area**\n",
    "\n",
    "$$A(X) = \\int_a^X f(t)\\,dt$$\n",
    "\n",
    "is a brand-new *function* of where you stop. Slide $X$: the top panel shades the area under $f$ from $a$ to $X$ (green), and the bottom panel plots $A(X)$ as it grows, point by point. Watch what the accumulated-area curve looks like — its **slope** at $X$ is exactly the **height** $f(X)$ feeding the area. That is the Fundamental Theorem of Calculus sneaking up on you, one chapter early *(Problem 34)*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5b6fc17c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# (f, a, b) per preset \\u2014 the accumulator runs the left edge at a.\n",
    "_ACCUM = {\n",
    "    'f(t) = t':            (lambda t: t,          0.0, 3.0),\n",
    "    'f(t) = t^2':          (lambda t: t**2,       0.0, 2.0),\n",
    "    'f(t) = sin(t)':       (lambda t: np.sin(t),  0.0, 2*np.pi),\n",
    "    'f(t) = cos(t)':       (lambda t: np.cos(t),  0.0, 2*np.pi),\n",
    "    'f(t) = e^{-t^2}':     (lambda t: np.exp(-t**2), -2.0, 2.0),\n",
    "}\n",
    "\n",
    "def _accumulate(curve='f(t) = t^2', X=1.0):\n",
    "    f, a, b = _ACCUM[curve]\n",
    "    X = min(max(X, a), b)\n",
    "    ts = np.linspace(a, b, 600)\n",
    "    ts_fill = np.linspace(a, X, 300)\n",
    "    # A(x) for every x in [a, b], by cumulative trapezoid integration.\n",
    "    grid = np.linspace(a, b, 600)\n",
    "    fg = f(grid)\n",
    "    A = np.concatenate(([0.0], np.cumsum(\n",
    "        0.5 * (fg[1:] + fg[:-1]) * np.diff(grid))))\n",
    "    A_at_X = float(np.interp(X, grid, A))\n",
    "\n",
    "    fig, (axT, axB) = plt.subplots(2, 1, figsize=(7.6, 6.2),\n",
    "                                   sharex=True)\n",
    "    # Top: f with the running area shaded to X.\n",
    "    axT.plot(ts, f(ts), color=BLUE, lw=2.6, label='$f(t)$')\n",
    "    axT.fill_between(ts_fill, f(ts_fill), color=SHADEG, alpha=0.85,\n",
    "                     zorder=1, label='area so far')\n",
    "    axT.axvline(X, color=RED, lw=1.4, ls=':')\n",
    "    axT.scatter([X], [f(X)], s=55, color=RED, zorder=5,\n",
    "                label=f'height $f(X) = {float(f(X)):.3f}$')\n",
    "    axT.axhline(0, color=GRAY, lw=0.8)\n",
    "    axT.set_ylabel('$y$')\n",
    "    axT.set_title(f'Area under $f$ from {a:.2f} to $X$', color=DARK)\n",
    "    axT.legend(loc='upper left', framealpha=0.9, fontsize=9)\n",
    "    # Bottom: the accumulated area A(X), drawn up to X.\n",
    "    upto = grid <= X\n",
    "    axB.plot(grid[upto], A[upto], color=GREEN, lw=2.6,\n",
    "             label='$A(X) = \\\\int_a^X f$')\n",
    "    axB.plot(grid[~upto], A[~upto], color=LIGHT, lw=2.0, ls='--',\n",
    "             zorder=1)\n",
    "    axB.scatter([X], [A_at_X], s=55, color=RED, zorder=5)\n",
    "    axB.axvline(X, color=RED, lw=1.4, ls=':')\n",
    "    axB.axhline(0, color=GRAY, lw=0.8)\n",
    "    axB.set_xlabel('$X$'); axB.set_ylabel('area')\n",
    "    axB.set_title(f'Accumulated area  $A(X) = {A_at_X:.4f}$',\n",
    "                  color=DARK)\n",
    "    axB.legend(loc='upper left', framealpha=0.9, fontsize=10)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "interact(_accumulate,\n",
    "         curve=Dropdown(options=list(_ACCUM), description='f'),\n",
    "         X=FloatSlider(value=1.0, min=-2.0, max=2*np.pi, step=0.1,\n",
    "                       description='right X'));\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "88e57170",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "*Four pictures, two faces. Widgets 1 and 4 are the **area** face — the integral as a limit of rectangles, and the area as a function of its right edge. Widgets 2 and 3 are the **antiderivative** face — undoing a derivative, and the chain rule run backward. They look unrelated, and they share a symbol. Chapter 11 reveals why: the slope of the accumulated-area curve in Widget 4 was $f$ all along.*"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}