{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "84f1d2f2",
   "metadata": {},
   "source": [
    "# DCT Laboratory — Volume I, Chapter 4\n",
    "## Enterprise Modeling Principles\n",
    "**Seed `26104`** · Companion to the chapter and AXIOM Module **AXIOM-04**\n",
    "\n",
    "Two of the chapter's principles, made numerical: the **representation bound** — a\n",
    "richer model is not a better model once you leave the calibration window — and the\n",
    "**Hierarchical Consistency Theorem** — coarse dynamics are legitimate exactly when\n",
    "the fine rates agree. Mirrored in `DCT_V1_Ch04_Lab.xlsx`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0f412239",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:08:45.859678Z",
     "iopub.status.busy": "2026-07-13T22:08:45.859513Z",
     "iopub.status.idle": "2026-07-13T22:08:46.850020Z",
     "shell.execute_reply": "2026-07-13T22:08:46.848754Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "plt.rcParams['figure.dpi']=110\n",
    "\n",
    "import numpy as np\n",
    "SEED = 26104\n",
    "T = np.arange(21.0)                    # quarters 0..20\n",
    "TRUE = 50 + 30/(1 + np.exp(-0.5*(T - 8)))\n",
    "rng = np.random.default_rng(SEED)\n",
    "NOISE = np.round(rng.normal(0, 1.2, 21), 4)   # frozen measurement noise (shipped as constants)\n",
    "OBS = TRUE + NOISE\n",
    "CAL, HOLD = slice(0, 13), slice(13, 21)\n",
    "\n",
    "def fit_poly(deg):\n",
    "    c = np.polyfit(T[CAL], OBS[CAL], deg)\n",
    "    return np.round(c, 6)\n",
    "\n",
    "C_LIN = fit_poly(1)     # the simple model\n",
    "C_RICH = fit_poly(6)    # the over-rich model\n",
    "\n",
    "def rmse(coeffs, window):\n",
    "    pred = np.polyval(coeffs, T[window])\n",
    "    return float(np.sqrt(np.mean((OBS[window] - pred)**2)))\n",
    "\n",
    "# Hierarchical consistency: three subsystems x_i' = a_i x_i, coarse X = sum(x)\n",
    "X0 = np.array([40.0, 35.0, 25.0])\n",
    "def agg_paths(a, n=20, dt=0.25):\n",
    "    xs = np.empty((n+1, 3)); xs[0] = X0\n",
    "    for k in range(n): xs[k+1] = xs[k] + dt*a*xs[k]\n",
    "    fine = xs.sum(axis=1)\n",
    "    a_bar = float((a*X0).sum()/X0.sum())      # naive coarse rate\n",
    "    coarse = np.empty(n+1); coarse[0] = X0.sum()\n",
    "    for k in range(n): coarse[k+1] = coarse[k] + dt*a_bar*coarse[k]\n",
    "    return fine, coarse\n",
    "\n",
    "A_EQ  = np.array([0.06, 0.06, 0.06])          # fiber-invariant: coarse law exact\n",
    "A_NEQ = np.array([0.12, 0.02, -0.04])         # not: coarse law drifts\n",
    "\n",
    "def reference_values():\n",
    "    fe, ce = agg_paths(A_EQ); fn, cn = agg_paths(A_NEQ)\n",
    "    return {\n",
    "        \"rmse_simple_calib\":  round(rmse(C_LIN, CAL), 4),\n",
    "        \"rmse_rich_calib\":    round(rmse(C_RICH, CAL), 4),\n",
    "        \"rmse_simple_holdout\":round(rmse(C_LIN, HOLD), 4),\n",
    "        \"rmse_rich_holdout\":  round(rmse(C_RICH, HOLD), 4),\n",
    "        \"agg_gap_equal_T\":    round(abs(fe[-1]-ce[-1]), 4),\n",
    "        \"agg_gap_unequal_T\":  round(abs(fn[-1]-cn[-1]), 4),\n",
    "        \"obs_calib_mean\":     round(float(OBS[CAL].mean()), 4),\n",
    "    }\n",
    "if __name__ == \"__main__\":\n",
    "    [print(f\"{k:22s} {v}\") for k,v in reference_values().items()]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5acd108f",
   "metadata": {},
   "source": [
    "## Panel 1 — The validation lesson\n",
    "A logistic adoption curve observed with measurement noise. Two models fit on\n",
    "quarters 0–12: **simple** (linear) and **rich** (degree-6 polynomial). In-sample the\n",
    "rich model wins; on the holdout (quarters 13–20) it detonates. Validation on a\n",
    "declared record (Model Validation Theorem) is what catches this — in-sample fit\n",
    "never can."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "45ca30af",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:08:46.852070Z",
     "iopub.status.busy": "2026-07-13T22:08:46.851805Z",
     "iopub.status.idle": "2026-07-13T22:08:47.156938Z",
     "shell.execute_reply": "2026-07-13T22:08:47.155333Z"
    }
   },
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(8.4,4.4))\n",
    "tt = np.linspace(0,20,300)\n",
    "ax.scatter(T[CAL], OBS[CAL], c=\"#0B3D2E\", s=28, label=\"calibration obs\")\n",
    "ax.scatter(T[HOLD], OBS[HOLD], facecolors=\"none\", edgecolors=\"#0B3D2E\", s=34, label=\"holdout obs\")\n",
    "ax.plot(tt, np.polyval(C_LIN, tt), c=\"#C8A24B\", lw=2.2, label=\"simple (linear)\")\n",
    "ax.plot(tt, np.polyval(C_RICH, tt), c=\"#1B6B52\", lw=2.2, ls=\"--\", label=\"rich (degree 6)\")\n",
    "ax.set(xlabel=\"quarter\", ylabel=\"enterprise output\", ylim=(30,110),\n",
    "       title=\"The representation bound: richer ≠ better out of sample (seed 26104)\")\n",
    "ax.axvspan(12.5, 20.5, color=\"#C8A24B\", alpha=.08)\n",
    "ax.legend(frameon=False); ax.grid(alpha=.25); plt.tight_layout(); plt.show()\n",
    "for nm,c in [(\"simple\",C_LIN),(\"rich\",C_RICH)]:\n",
    "    print(f\"{nm:7s} RMSE  calib {rmse(c,CAL):8.4f}   holdout {rmse(c,HOLD):10.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e40c1fb6",
   "metadata": {},
   "source": [
    "## Panel 2 — Hierarchical consistency\n",
    "Three subsystems growing at rates $a_i$; the coarse state is their sum with a naive\n",
    "averaged rate. **Equal rates** (fiber-invariant): the coarse law is exact, gap = 0.\n",
    "**Unequal rates**: the coarse law drifts as composition shifts — the aggregate is\n",
    "not a state (Hierarchical Consistency Theorem)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7bb8006b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:08:47.159532Z",
     "iopub.status.busy": "2026-07-13T22:08:47.158777Z",
     "iopub.status.idle": "2026-07-13T22:08:47.496221Z",
     "shell.execute_reply": "2026-07-13T22:08:47.495170Z"
    }
   },
   "outputs": [],
   "source": [
    "t = np.arange(21)*0.25\n",
    "fig, axes = plt.subplots(1,2, figsize=(9.6,3.9), sharey=False)\n",
    "for ax,(a,ttl) in zip(axes,[(A_EQ,\"equal rates — exact\"),(A_NEQ,\"unequal rates — drifting\")]):\n",
    "    fine, coarse = agg_paths(a)\n",
    "    ax.plot(t, fine, c=\"#0B3D2E\", lw=2.2, label=\"sum of fine states\")\n",
    "    ax.plot(t, coarse, c=\"#C8A24B\", lw=2.2, ls=\"--\", label=\"coarse law\")\n",
    "    ax.set(title=ttl, xlabel=\"years\"); ax.legend(frameon=False); ax.grid(alpha=.25)\n",
    "plt.tight_layout(); plt.show()\n",
    "fe,ce = agg_paths(A_EQ); fn,cn = agg_paths(A_NEQ)\n",
    "print(\"terminal gap, equal rates  :\", round(abs(fe[-1]-ce[-1]),4))\n",
    "print(\"terminal gap, unequal rates:\", round(abs(fn[-1]-cn[-1]),4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41716979",
   "metadata": {},
   "source": [
    "## Validation — agrees with `DCT_V1_Ch04_Lab.xlsx`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f89bba63",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:08:47.498082Z",
     "iopub.status.busy": "2026-07-13T22:08:47.497846Z",
     "iopub.status.idle": "2026-07-13T22:08:47.504244Z",
     "shell.execute_reply": "2026-07-13T22:08:47.503301Z"
    }
   },
   "outputs": [],
   "source": [
    "ref = reference_values()\n",
    "expected = {\"rmse_simple_calib\":2.2016,\"rmse_rich_calib\":1.2348,\"rmse_simple_holdout\":6.4713,\n",
    " \"rmse_rich_holdout\":241.9653,\"agg_gap_equal_T\":0.0,\"agg_gap_unequal_T\":6.2884,\"obs_calib_mean\":60.9148}\n",
    "for k,v in expected.items():\n",
    "    assert abs(ref[k]-v)<5e-4, f\"MISMATCH {k}: {ref[k]} vs {v}\"\n",
    "    print(f\"PASS  {k:22s} {ref[k]}\")\n",
    "print(\"\\nAll checkpoints agree — seed 26104.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76a1075d",
   "metadata": {},
   "source": [
    "**Next**: Exercises 4.9–4.12 (Part C); AXIOM-04's model bench lets you slide the polynomial degree and watch the holdout error turn. Solutions: IM Ch. 4."
   ]
  }
 ],
 "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.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
