{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e4938327",
   "metadata": {},
   "source": [
    "# What learning is · Not everything is a learning problem\n",
    "\n",
    "The robot's habit is **fixed**. The same push, every shot, forever.\n",
    "\n",
    "Which raises a question nobody asked in the first lesson: why iterate at all?\n",
    "Shoot ten arrows, average where they landed, subtract that from the aim. Done\n",
    "— no loop, no coach, no learning rate, no retraining.\n",
    "\n",
    "This notebook runs that against the learner twice: once in a world that holds\n",
    "still, and once in a world that moves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "86f76370",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T07:08:35.238211Z",
     "iopub.status.busy": "2026-08-19T07:08:35.237945Z",
     "iopub.status.idle": "2026-08-19T07:08:35.731090Z",
     "shell.execute_reply": "2026-08-19T07:08:35.730828Z"
    }
   },
   "outputs": [],
   "source": [
    "# hide — plumbing so this file runs both in the repo and in Colab.\n",
    "try:\n",
    "    from _figkit import save_fig, record\n",
    "except ImportError:  # Colab — no repo, no problem\n",
    "    def save_fig(name, plot, **kw):\n",
    "        import matplotlib.pyplot as plt\n",
    "        fig, ax = plt.subplots(figsize=kw.get('figsize', (7, 4.2))); plot(ax); plt.show()\n",
    "    def record(key, value):\n",
    "        print(f'{key} = {value}'); return value"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d9b0a9a",
   "metadata": {},
   "source": [
    "## 1. The same bow, and two ways to deal with it\n",
    "\n",
    "**The learner** is the robot from lesson one: after every arrow it nudges its\n",
    "aim a fraction of the way back.\n",
    "\n",
    "**The rule** does not learn at all after its first ten arrows. It measures the\n",
    "habit once, subtracts it, and never touches the aim again."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2b1bf96f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T07:08:35.732390Z",
     "iopub.status.busy": "2026-08-19T07:08:35.732285Z",
     "iopub.status.idle": "2026-08-19T07:08:35.737351Z",
     "shell.execute_reply": "2026-08-19T07:08:35.737019Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "measure_arrows = 10\n",
      "arrows = 60\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "60"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: One bow. One robot that keeps adjusting, one that measures once and stops.\n",
    "import random\n",
    "\n",
    "BIAS = (-11.0, 7.0)\n",
    "WOBBLE = 4.0\n",
    "ARROWS = 60\n",
    "TRIALS = 400\n",
    "STEP = 0.15\n",
    "MEASURE = 10            # arrows the rule spends working out the habit\n",
    "\n",
    "def miss_by(shot):\n",
    "    return (shot[0] ** 2 + shot[1] ** 2) ** 0.5\n",
    "\n",
    "def learner(seed, bias_at):\n",
    "    rng = random.Random(seed)\n",
    "    aim, out = [0.0, 0.0], []\n",
    "    for i in range(ARROWS):\n",
    "        bx, by = bias_at(i)\n",
    "        shot = (aim[0] + bx + rng.gauss(0, WOBBLE), aim[1] + by + rng.gauss(0, WOBBLE))\n",
    "        out.append(miss_by(shot))\n",
    "        aim[0] -= STEP * shot[0]\n",
    "        aim[1] -= STEP * shot[1]\n",
    "    return out\n",
    "\n",
    "def rule(seed, bias_at):\n",
    "    rng = random.Random(seed)\n",
    "    aim, out, seen = [0.0, 0.0], [], []\n",
    "    for i in range(ARROWS):\n",
    "        bx, by = bias_at(i)\n",
    "        shot = (aim[0] + bx + rng.gauss(0, WOBBLE), aim[1] + by + rng.gauss(0, WOBBLE))\n",
    "        out.append(miss_by(shot))\n",
    "        if i < MEASURE:\n",
    "            seen.append(shot)\n",
    "            if i == MEASURE - 1:   # measured. subtract it. never adjust again.\n",
    "                aim = [-sum(s[0] for s in seen) / MEASURE, -sum(s[1] for s in seen) / MEASURE]\n",
    "    return out\n",
    "\n",
    "def average(robot, bias_at):\n",
    "    runs = [robot(s, bias_at) for s in range(TRIALS)]\n",
    "    return [sum(r[i] for r in runs) / TRIALS for i in range(ARROWS)]\n",
    "\n",
    "record('measure_arrows', MEASURE)\n",
    "record('arrows', ARROWS)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24e55a1b",
   "metadata": {},
   "source": [
    "## 2. A world that holds still\n",
    "\n",
    "The habit never changes. Both approaches get the same sixty arrows."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "2d8fa61f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T07:08:35.738566Z",
     "iopub.status.busy": "2026-08-19T07:08:35.738431Z",
     "iopub.status.idle": "2026-08-19T07:08:35.859456Z",
     "shell.execute_reply": "2026-08-19T07:08:35.859197Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "floor = 5.0\n",
      "rule_settled = 5.2\n",
      "learner_settled = 5.3\n",
      "learner_at_measure = 5.8\n",
      "rule_at_measure = 5.4\n",
      "after the first 10 arrows — rule 5.2 cm, learner 5.3 cm, floor 5.0 cm\n",
      "figure rule-against-learner -> not-everything-is-a-learning-problem.rule-against-learner.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: The learner against three lines of arithmetic\n",
    "still = lambda i: BIAS\n",
    "\n",
    "learn_still = average(learner, still)\n",
    "rule_still = average(rule, still)\n",
    "\n",
    "rng = random.Random(99)\n",
    "FLOOR = sum(miss_by((rng.gauss(0, WOBBLE), rng.gauss(0, WOBBLE))) for _ in range(20000)) / 20000\n",
    "\n",
    "def settled(curve, frm=MEASURE + 1):\n",
    "    return sum(curve[frm:]) / len(curve[frm:])\n",
    "\n",
    "record('floor', round(FLOOR, 1))\n",
    "record('rule_settled', round(settled(rule_still), 1))\n",
    "record('learner_settled', round(settled(learn_still), 1))\n",
    "record('learner_at_measure', round(learn_still[MEASURE], 1))\n",
    "record('rule_at_measure', round(rule_still[MEASURE], 1))\n",
    "\n",
    "print(f'after the first {MEASURE} arrows — rule {settled(rule_still):.1f} cm, '\n",
    "      f'learner {settled(learn_still):.1f} cm, floor {FLOOR:.1f} cm')\n",
    "\n",
    "def plot_still(ax):\n",
    "    x = range(1, ARROWS + 1)\n",
    "    ax.plot(x, learn_still, color='#9aa1ab', linewidth=2.2, label='the learner')\n",
    "    ax.plot(x, rule_still, color='#ee785b', linewidth=2.4, label='measure once, subtract')\n",
    "    ax.axhline(FLOOR, color='#6b7280', linestyle=':', linewidth=1.2)\n",
    "    ax.axvline(MEASURE, color='#b9b2aa', linewidth=1, linestyle='--')\n",
    "    ax.text(MEASURE + 1, max(learn_still) * 0.92, 'habit measured', fontsize=9.5, color='#6b7280')\n",
    "    ax.set_xlabel('arrows shot')\n",
    "    ax.set_ylabel('distance from the bullseye (cm)')\n",
    "    ax.set_ylim(0, max(learn_still) * 1.12)\n",
    "    ax.legend(frameon=False, fontsize=10)\n",
    "    ax.spines[['top', 'right']].set_visible(False)\n",
    "\n",
    "save_fig('rule-against-learner', plot_still, figsize=(7.4, 4.0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "962639b0",
   "metadata": {},
   "source": [
    "## 3. A world that moves\n",
    "\n",
    "Same experiment, one change: a crosswind arrives at arrow thirty and does not\n",
    "leave. Nothing is told about it — it simply starts happening.\n",
    "\n",
    "The rule measured a habit that no longer exists."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3c67facd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T07:08:35.860673Z",
     "iopub.status.busy": "2026-08-19T07:08:35.860590Z",
     "iopub.status.idle": "2026-08-19T07:08:35.957923Z",
     "shell.execute_reply": "2026-08-19T07:08:35.957695Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "shift_at = 30\n",
      "rule_after_shift = 11.6\n",
      "learner_after_shift = 6.1\n",
      "rule_end = 11.9\n",
      "learner_end = 5.3\n",
      "learner_recovers_in = 9\n",
      "from arrow 30 on — rule 11.6 cm, learner 6.1 cm\n",
      "learner is back within a centimetre of the floor after 9 arrows\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure when-the-world-moves -> not-everything-is-a-learning-problem.when-the-world-moves.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: The same two, after the wind changes at arrow thirty\n",
    "SHIFT_AT = 30\n",
    "WIND = (9.0, -6.0)\n",
    "\n",
    "def windy(i):\n",
    "    return BIAS if i < SHIFT_AT else (BIAS[0] + WIND[0], BIAS[1] + WIND[1])\n",
    "\n",
    "learn_wind = average(learner, windy)\n",
    "rule_wind = average(rule, windy)\n",
    "\n",
    "after = lambda c: sum(c[SHIFT_AT:]) / len(c[SHIFT_AT:])\n",
    "record('shift_at', SHIFT_AT)\n",
    "record('rule_after_shift', round(after(rule_wind), 1))\n",
    "record('learner_after_shift', round(after(learn_wind), 1))\n",
    "record('rule_end', round(rule_wind[-1], 1))\n",
    "record('learner_end', round(learn_wind[-1], 1))\n",
    "\n",
    "# How long the learner takes to get back to within a centimetre of the floor.\n",
    "back = next((i - SHIFT_AT + 1 for i in range(SHIFT_AT, ARROWS) if learn_wind[i] < FLOOR + 1), None)\n",
    "record('learner_recovers_in', back)\n",
    "\n",
    "print(f'from arrow {SHIFT_AT} on — rule {after(rule_wind):.1f} cm, learner {after(learn_wind):.1f} cm')\n",
    "print(f'learner is back within a centimetre of the floor after {back} arrows')\n",
    "\n",
    "def plot_wind(ax):\n",
    "    x = range(1, ARROWS + 1)\n",
    "    ax.plot(x, learn_wind, color='#2f9e6e', linewidth=2.4, label='the learner')\n",
    "    ax.plot(x, rule_wind, color='#ee785b', linewidth=2.4, label='measure once, subtract')\n",
    "    ax.axhline(FLOOR, color='#6b7280', linestyle=':', linewidth=1.2)\n",
    "    ax.axvline(SHIFT_AT, color='#b9b2aa', linewidth=1, linestyle='--')\n",
    "    ax.text(SHIFT_AT + 1, max(rule_wind) * 0.9, 'the wind changes', fontsize=9.5, color='#6b7280')\n",
    "    ax.set_xlabel('arrows shot')\n",
    "    ax.set_ylabel('distance from the bullseye (cm)')\n",
    "    ax.set_ylim(0, max(rule_wind) * 1.12)\n",
    "    ax.legend(frameon=False, fontsize=10)\n",
    "    ax.spines[['top', 'right']].set_visible(False)\n",
    "\n",
    "save_fig('when-the-world-moves', plot_wind, figsize=(7.4, 4.0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4bb09ab",
   "metadata": {},
   "source": [
    "## 4. What this is and is not saying\n",
    "\n",
    "Averaging ten misses is arguably the smallest possible model — one parameter,\n",
    "fitted once. So the honest question was never *model or no model*. It is **how\n",
    "much machinery does this problem actually need**, and the answer here was: much\n",
    "less than the first lesson implied, right up until the world moved.\n",
    "\n",
    "The tasks that need no model at all are the ones where the answer is *defined*\n",
    "rather than discovered — tax on an invoice, which tier a discount falls in,\n",
    "whether two records match exactly. Nothing in this notebook can demonstrate\n",
    "those, because there is nothing to measure. That is the point of them."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
