{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": [
     "field-notebook-cover",
     "field-notebook-style"
    ]
   },
   "source": [
    "<style>\n",
    "/*\n",
    " * The Evidence Path\n",
    " * A restrained, dependency-free reading layer for Jupyter and the web.\n",
    " */\n",
    "\n",
    ":root {\n",
    "  --fn-paper: #ffffff;\n",
    "  --fn-ink: #111111;\n",
    "  --fn-ink-soft: #666666;\n",
    "  --fn-intro: #555555;\n",
    "  --fn-rule: #dddddd;\n",
    "  --fn-rule-strong: #111111;\n",
    "  --fn-code-paper: #f7f7f7;\n",
    "  --fn-code-ink: #191919;\n",
    "  --fn-code-leading: 1.5;\n",
    "  --fn-line-number: #888888;\n",
    "  --fn-line-number-width: 2.5rem;\n",
    "  --fn-code-gutter: 3.5rem;\n",
    "  --fn-syntax-name: #191919;\n",
    "  --fn-syntax-keyword: #5a3b73;\n",
    "  --fn-syntax-builtin: #315f75;\n",
    "  --fn-syntax-string: #2f6b43;\n",
    "  --fn-syntax-number: #8a4d20;\n",
    "  --fn-syntax-comment: #777777;\n",
    "  --fn-syntax-operator: #555555;\n",
    "  --fn-syntax-decorator: #7a4e20;\n",
    "  --fn-page-width: 42rem;\n",
    "  --fn-wide-width: 64rem;\n",
    "  --fn-measure: 38rem;\n",
    "  --fn-cell-top: 0;\n",
    "  --fn-cell-bottom: 0;\n",
    "  --fn-ui: Arial, Helvetica, sans-serif;\n",
    "  --fn-prose: Georgia, \"Times New Roman\", serif;\n",
    "  --fn-mono: ui-monospace, \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo,\n",
    "    monospace;\n",
    "}\n",
    "\n",
    "html {\n",
    "  color: var(--fn-ink);\n",
    "  background: var(--fn-paper);\n",
    "}\n",
    "\n",
    ".jp-Notebook,\n",
    ".jp-NotebookPanel-notebook,\n",
    ".notebook_app .container,\n",
    ".notebook-container {\n",
    "  color: var(--fn-ink) !important;\n",
    "  background: var(--fn-paper) !important;\n",
    "}\n",
    "\n",
    ".jp-Notebook .jp-Cell,\n",
    ".cell {\n",
    "  box-sizing: border-box;\n",
    "  position: relative;\n",
    "  max-width: var(--fn-page-width);\n",
    "  margin: 0 auto !important;\n",
    "  border: 0 !important;\n",
    "  box-shadow: none !important;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon,\n",
    ".text_cell_render {\n",
    "  box-sizing: border-box;\n",
    "  color: var(--fn-ink) !important;\n",
    "  background: var(--fn-paper) !important;\n",
    "  border: 0 !important;\n",
    "  font-family: var(--fn-prose) !important;\n",
    "  font-size: 1.08rem;\n",
    "  line-height: 1.64;\n",
    "  padding: var(--fn-cell-top) 0 var(--fn-cell-bottom) !important;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon p,\n",
    ".text_cell_render p {\n",
    "  max-width: var(--fn-measure);\n",
    "  margin: 0 0 1.35rem;\n",
    "  text-wrap: pretty;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon h1,\n",
    ".jp-RenderedHTMLCommon h2,\n",
    ".jp-RenderedHTMLCommon h3,\n",
    ".jp-RenderedHTMLCommon h4,\n",
    ".text_cell_render h1,\n",
    ".text_cell_render h2,\n",
    ".text_cell_render h3,\n",
    ".text_cell_render h4 {\n",
    "  color: var(--fn-ink) !important;\n",
    "  border: 0 !important;\n",
    "  font-family: var(--fn-ui) !important;\n",
    "  font-weight: 700;\n",
    "  text-wrap: balance;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon h1,\n",
    ".text_cell_render h1 {\n",
    "  font-size: clamp(2rem, 7vw, 3.5rem);\n",
    "  letter-spacing: -0.04em;\n",
    "  line-height: 1.08;\n",
    "  margin: 0 0 1rem;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon h2,\n",
    ".text_cell_render h2 {\n",
    "  font-size: 1.65rem;\n",
    "  letter-spacing: -0.025em;\n",
    "  line-height: 1.2;\n",
    "  margin: 3.25rem 0 1rem;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon h3,\n",
    ".text_cell_render h3 {\n",
    "  font-size: 1.2rem;\n",
    "  letter-spacing: -0.015em;\n",
    "  line-height: 1.3;\n",
    "  margin: 2.25rem 0 0.75rem;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon h4,\n",
    ".text_cell_render h4 {\n",
    "  font-size: 1rem;\n",
    "  letter-spacing: 0;\n",
    "  line-height: 1.35;\n",
    "  margin: 1.75rem 0 0.65rem;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon > :first-child,\n",
    ".text_cell_render > :first-child {\n",
    "  margin-top: 0;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon a,\n",
    ".text_cell_render a {\n",
    "  color: inherit !important;\n",
    "  text-decoration: underline;\n",
    "  text-decoration-thickness: 1px;\n",
    "  text-underline-offset: 0.18em;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon a:hover,\n",
    ".text_cell_render a:hover {\n",
    "  text-decoration-thickness: 2px;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon a:focus-visible,\n",
    ".text_cell_render a:focus-visible {\n",
    "  outline: 2px solid currentColor;\n",
    "  outline-offset: 3px;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon strong,\n",
    ".text_cell_render strong {\n",
    "  color: var(--fn-ink);\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon blockquote,\n",
    ".text_cell_render blockquote {\n",
    "  max-width: 36rem;\n",
    "  margin: 1.75rem 0;\n",
    "  padding: 0 0 0 1.25rem;\n",
    "  border-left: 2px solid var(--fn-rule-strong) !important;\n",
    "  color: #444444 !important;\n",
    "  font-style: italic;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon blockquote p:last-child,\n",
    ".text_cell_render blockquote p:last-child {\n",
    "  margin-bottom: 0;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon ul,\n",
    ".jp-RenderedHTMLCommon ol,\n",
    ".text_cell_render ul,\n",
    ".text_cell_render ol {\n",
    "  max-width: var(--fn-measure);\n",
    "  margin: 0 0 1.5rem;\n",
    "  padding-left: 1.5rem;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon li,\n",
    ".text_cell_render li {\n",
    "  margin: 0.25rem 0;\n",
    "  padding-left: 0.15rem;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon table,\n",
    ".text_cell_render table {\n",
    "  display: table;\n",
    "  width: 100%;\n",
    "  margin: 1.75rem 0 2rem;\n",
    "  border-collapse: collapse;\n",
    "  border-spacing: 0;\n",
    "  color: var(--fn-ink);\n",
    "  background: transparent;\n",
    "  font-family: var(--fn-ui);\n",
    "  font-size: 0.88rem;\n",
    "  line-height: 1.45;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon th,\n",
    ".jp-RenderedHTMLCommon td,\n",
    ".text_cell_render th,\n",
    ".text_cell_render td {\n",
    "  padding: 0.65rem 0.5rem !important;\n",
    "  border: 0 !important;\n",
    "  border-bottom: 1px solid var(--fn-rule) !important;\n",
    "  text-align: left !important;\n",
    "  vertical-align: top !important;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon th,\n",
    ".text_cell_render th {\n",
    "  border-bottom-color: var(--fn-rule-strong) !important;\n",
    "  font-weight: 700 !important;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon code,\n",
    ".text_cell_render code {\n",
    "  color: var(--fn-code-ink) !important;\n",
    "  background: transparent !important;\n",
    "  border: 0;\n",
    "  font-family: var(--fn-mono) !important;\n",
    "  font-size: 0.85em;\n",
    "  padding: 0;\n",
    "}\n",
    "\n",
    ".jp-RenderedHTMLCommon pre,\n",
    ".text_cell_render pre {\n",
    "  box-sizing: border-box;\n",
    "  max-width: 100%;\n",
    "  margin: 1.5rem 0 2rem;\n",
    "  overflow-x: auto;\n",
    "  color: var(--fn-code-ink);\n",
    "  background: var(--fn-code-paper);\n",
    "  border: 1px solid var(--fn-rule);\n",
    "  font-family: var(--fn-mono);\n",
    "  font-size: 0.8rem;\n",
    "  line-height: var(--fn-code-leading);\n",
    "  padding: 1rem;\n",
    "  tab-size: 4;\n",
    "  white-space: pre;\n",
    "}\n",
    "\n",
    ".jp-CodeCell,\n",
    ".code_cell,\n",
    ".workbench-note,\n",
    ".bench-insert {\n",
    "  box-sizing: border-box;\n",
    "  width: 100%;\n",
    "  max-width: var(--fn-page-width) !important;\n",
    "  margin: 1.75rem auto 2rem !important;\n",
    "  padding: 0 !important;\n",
    "  transform: none;\n",
    "  background: var(--fn-paper) !important;\n",
    "  border: 0 !important;\n",
    "}\n",
    "\n",
    ".jp-CodeCell .jp-InputArea,\n",
    ".code_cell .input_area {\n",
    "  box-sizing: border-box;\n",
    "  max-width: 100%;\n",
    "  overflow-x: auto;\n",
    "  background: var(--fn-code-paper) !important;\n",
    "  border: 1px solid var(--fn-rule) !important;\n",
    "}\n",
    "\n",
    ".jp-CodeCell .jp-InputArea pre,\n",
    ".code_cell .input_area pre,\n",
    ".jp-CodeCell .jp-InputArea code,\n",
    ".code_cell .input_area code {\n",
    "  margin: 0 !important;\n",
    "  color: var(--fn-code-ink) !important;\n",
    "  background: transparent !important;\n",
    "  border: 0 !important;\n",
    "  font-family: var(--fn-mono) !important;\n",
    "  font-size: 0.8rem !important;\n",
    "  line-height: var(--fn-code-leading) !important;\n",
    "  padding: 1rem !important;\n",
    "  white-space: pre !important;\n",
    "}\n",
    "\n",
    ".jp-OutputArea,\n",
    ".output_area,\n",
    ".output-slip {\n",
    "  box-sizing: border-box;\n",
    "  max-width: 100%;\n",
    "  margin: 0.85rem 0 0 !important;\n",
    "  padding: 0.15rem 0 0.15rem 1rem !important;\n",
    "  overflow-x: auto;\n",
    "  color: var(--fn-code-ink) !important;\n",
    "  background: transparent !important;\n",
    "  border: 0 !important;\n",
    "  border-left: 2px solid var(--fn-rule) !important;\n",
    "}\n",
    "\n",
    ".jp-OutputArea pre,\n",
    ".output_area pre,\n",
    ".output-slip pre {\n",
    "  margin: 0.45rem 0 0 !important;\n",
    "  overflow-x: auto;\n",
    "  color: var(--fn-code-ink) !important;\n",
    "  background: transparent !important;\n",
    "  border: 0 !important;\n",
    "  font-family: var(--fn-mono) !important;\n",
    "  font-size: 0.8rem !important;\n",
    "  line-height: 1.5 !important;\n",
    "  padding: 0 !important;\n",
    "  white-space: pre-wrap;\n",
    "}\n",
    "\n",
    ".field-cover {\n",
    "  margin: 0 0 2rem;\n",
    "  padding: 2rem 0 1.75rem;\n",
    "  border-bottom: 1px solid var(--fn-rule-strong);\n",
    "}\n",
    "\n",
    ".cover-kicker,\n",
    ".folio-kicker {\n",
    "  color: var(--fn-ink-soft);\n",
    "  font-family: var(--fn-ui);\n",
    "  font-size: 0.82rem;\n",
    "  letter-spacing: 0;\n",
    "  line-height: 1.4;\n",
    "}\n",
    "\n",
    ".cover-kicker {\n",
    "  display: none;\n",
    "}\n",
    "\n",
    ".cover-subtitle {\n",
    "  max-width: 36rem;\n",
    "  margin: 0 0 1rem;\n",
    "  color: var(--fn-intro);\n",
    "  font-family: var(--fn-prose);\n",
    "  font-size: 1.25rem;\n",
    "  line-height: 1.5;\n",
    "}\n",
    "\n",
    ".cover-scribble {\n",
    "  display: none;\n",
    "}\n",
    "\n",
    ".folio-opener,\n",
    ".experiment-opener {\n",
    "  margin: 3.75rem 0 1.75rem;\n",
    "  padding: 1.5rem 0 0;\n",
    "  border-top: 2px solid var(--fn-rule-strong);\n",
    "}\n",
    "\n",
    ".folio-opener .folio-kicker,\n",
    ".experiment-opener .folio-kicker {\n",
    "  display: none;\n",
    "}\n",
    "\n",
    ".folio-opener h1,\n",
    ".experiment-opener h1 {\n",
    "  font-size: clamp(2rem, 6vw, 3rem) !important;\n",
    "  margin-bottom: 0.65rem !important;\n",
    "}\n",
    "\n",
    ".folio-opener > p,\n",
    ".experiment-opener > p {\n",
    "  color: var(--fn-intro);\n",
    "  font-size: 1.12rem;\n",
    "  margin-bottom: 1rem;\n",
    "}\n",
    "\n",
    ".opening-question,\n",
    ".field-question {\n",
    "  max-width: 36rem;\n",
    "  margin: 1.25rem 0 1.75rem;\n",
    "  color: #444444;\n",
    "  font-family: var(--fn-prose);\n",
    "  font-size: 1rem;\n",
    "  font-style: italic;\n",
    "  line-height: 1.6;\n",
    "}\n",
    "\n",
    ".leaf-download,\n",
    ".source-links {\n",
    "  font-family: var(--fn-ui);\n",
    "  font-size: 0.85rem;\n",
    "}\n",
    "\n",
    ".reading-ribbon,\n",
    ".insert-legend {\n",
    "  display: none;\n",
    "}\n",
    "\n",
    ".two-page-spread,\n",
    ".notebook-grid {\n",
    "  display: grid;\n",
    "  grid-template-columns: repeat(2, minmax(0, 1fr));\n",
    "  gap: 2rem;\n",
    "  margin: 2rem 0 2.5rem;\n",
    "  padding-top: 1.25rem;\n",
    "  border-top: 1px solid var(--fn-rule);\n",
    "}\n",
    "\n",
    ".two-page-spread h3,\n",
    ".notebook-grid h3 {\n",
    "  margin-top: 0 !important;\n",
    "}\n",
    "\n",
    ".margin-note,\n",
    ".research-note,\n",
    ".warning-note,\n",
    ".provenance-note {\n",
    "  max-width: 36rem;\n",
    "  margin: 1.75rem 0;\n",
    "  padding-left: 1.25rem;\n",
    "  border-left: 2px solid var(--fn-rule);\n",
    "  color: #444444;\n",
    "  font-size: 0.95rem;\n",
    "}\n",
    "\n",
    ".margin-note p:last-child,\n",
    ".research-note p:last-child,\n",
    ".warning-note p:last-child,\n",
    ".provenance-note p:last-child {\n",
    "  margin-bottom: 0;\n",
    "}\n",
    "\n",
    ".observation,\n",
    ".experiment,\n",
    ".definition-card,\n",
    ".execution-note {\n",
    "  margin: 2rem 0;\n",
    "  padding: 1.25rem 0;\n",
    "  border-top: 1px solid var(--fn-rule);\n",
    "  border-bottom: 1px solid var(--fn-rule);\n",
    "}\n",
    "\n",
    ".observation > :last-child,\n",
    ".experiment > :last-child,\n",
    ".definition-card > :last-child,\n",
    ".execution-note > :last-child {\n",
    "  margin-bottom: 0;\n",
    "}\n",
    "\n",
    ".sketch,\n",
    ".two-page-spread,\n",
    ".evidence-strip {\n",
    "  max-width: 100%;\n",
    "}\n",
    "\n",
    ".sketch {\n",
    "  margin: 2rem 0;\n",
    "}\n",
    "\n",
    ".sketch svg,\n",
    ".sketch img {\n",
    "  display: block;\n",
    "  width: 100%;\n",
    "  height: auto;\n",
    "}\n",
    "\n",
    ".sketch svg text {\n",
    "  fill: var(--fn-ink) !important;\n",
    "  font-family: var(--fn-ui) !important;\n",
    "}\n",
    "\n",
    ".sketch svg [stroke] {\n",
    "  stroke: var(--fn-ink) !important;\n",
    "}\n",
    "\n",
    ".sketch svg [filter] {\n",
    "  filter: none !important;\n",
    "}\n",
    "\n",
    ".sketch-caption {\n",
    "  margin-top: 0.65rem;\n",
    "  color: var(--fn-ink-soft);\n",
    "  font-family: var(--fn-ui);\n",
    "  font-size: 0.8rem;\n",
    "}\n",
    "\n",
    ".chapter-bridge {\n",
    "  margin: 2.5rem 0;\n",
    "  padding-top: 1.25rem;\n",
    "  border-top: 1px solid var(--fn-rule);\n",
    "  color: #444444;\n",
    "  font-style: italic;\n",
    "}\n",
    "\n",
    ".atlas-source,\n",
    ".evidence-leaf {\n",
    "  display: block;\n",
    "  margin: 3rem 0 1rem;\n",
    "  padding-top: 1rem;\n",
    "  border-top: 1px solid var(--fn-rule-strong);\n",
    "  color: var(--fn-ink-soft);\n",
    "  font-family: var(--fn-ui);\n",
    "  font-size: 0.78rem;\n",
    "  line-height: 1.45;\n",
    "}\n",
    "\n",
    ".atlas-source + h2,\n",
    ".evidence-leaf + h2 {\n",
    "  margin-top: 1rem !important;\n",
    "}\n",
    "\n",
    ".binding-placeholder,\n",
    ".binding-note {\n",
    "  display: block;\n",
    "  margin: 2.5rem 0;\n",
    "  padding: 1.25rem 0;\n",
    "  border-top: 1px solid var(--fn-rule-strong);\n",
    "  border-bottom: 1px solid var(--fn-rule);\n",
    "}\n",
    "\n",
    ".binding-placeholder strong,\n",
    ".binding-note h3 {\n",
    "  font-family: var(--fn-ui);\n",
    "}\n",
    "\n",
    ".binding-placeholder p,\n",
    ".binding-note p {\n",
    "  margin: 0.5rem 0 0.75rem;\n",
    "}\n",
    "\n",
    ".source-stamp,\n",
    ".tape-label,\n",
    ".binding-path,\n",
    ".source-leaf-path,\n",
    ".page-number {\n",
    "  color: var(--fn-ink-soft);\n",
    "  background: transparent;\n",
    "  font-family: var(--fn-mono);\n",
    "  font-size: 0.75rem;\n",
    "  font-weight: 400;\n",
    "}\n",
    "\n",
    ".run-note,\n",
    ".run-command {\n",
    "  margin-top: 1rem;\n",
    "  font-family: var(--fn-ui);\n",
    "  font-size: 0.82rem;\n",
    "}\n",
    "\n",
    ".equation-note,\n",
    ".math-display {\n",
    "  box-sizing: border-box;\n",
    "  width: 100%;\n",
    "  margin: 1.5rem 0 1.75rem;\n",
    "  overflow-x: auto;\n",
    "  text-align: center;\n",
    "}\n",
    "\n",
    ".math-inline {\n",
    "  white-space: normal;\n",
    "}\n",
    "\n",
    ".handwritten,\n",
    ".underlined-red {\n",
    "  color: inherit;\n",
    "  font-family: inherit;\n",
    "  text-decoration: none;\n",
    "}\n",
    "\n",
    ".ink-sheet,\n",
    ".source-sheet,\n",
    ".source-leaf-body,\n",
    ".binding-note-body {\n",
    "  background: transparent;\n",
    "  border: 0;\n",
    "}\n",
    "\n",
    ".anchor-alias {\n",
    "  position: absolute;\n",
    "  width: 1px;\n",
    "  height: 1px;\n",
    "  overflow: hidden;\n",
    "}\n",
    "\n",
    "@media (max-width: 32rem) {\n",
    "  .jp-RenderedHTMLCommon,\n",
    "  .text_cell_render {\n",
    "    font-size: 1rem;\n",
    "  }\n",
    "\n",
    "  .field-cover {\n",
    "    padding-top: 2rem;\n",
    "  }\n",
    "\n",
    "  .folio-opener,\n",
    "  .experiment-opener {\n",
    "    margin-top: 3rem;\n",
    "  }\n",
    "\n",
    "  .two-page-spread,\n",
    "  .notebook-grid {\n",
    "    grid-template-columns: 1fr;\n",
    "    gap: 0;\n",
    "  }\n",
    "\n",
    "  .jp-RenderedHTMLCommon table,\n",
    "  .text_cell_render table {\n",
    "    display: block;\n",
    "    overflow-x: auto;\n",
    "    white-space: nowrap;\n",
    "  }\n",
    "\n",
    "}\n",
    "\n",
    "@media print {\n",
    "  :root {\n",
    "    --fn-page-width: 100%;\n",
    "    --fn-wide-width: 100%;\n",
    "  }\n",
    "\n",
    "  .jp-CodeCell,\n",
    "  .code_cell,\n",
    "  .workbench-note,\n",
    "  .bench-insert {\n",
    "    width: 100%;\n",
    "  }\n",
    "\n",
    "  .leaf-download,\n",
    "  .source-links {\n",
    "    display: none;\n",
    "  }\n",
    "\n",
    "  a {\n",
    "    color: inherit !important;\n",
    "  }\n",
    "}\n",
    "\n",
    "</style>\n",
    "\n",
    "<div class=\"field-cover\">\n",
    "  <div class=\"cover-kicker\">LAB 05 · LEARNING</div>\n",
    "  <h1>Teaching Search to Choose</h1>\n",
    "  <div class=\"cover-subtitle\">Negatives, objectives, transformations, fusion, reranking, selection, and reward</div>\n",
    "  <div class=\"cover-scribble\">relevance is only useful when it survives downstream</div>\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The working page\n",
    "\n",
    "This lab follows the learning signal through a modern retrieval stack. It\n",
    "treats negatives, score calibration, reranking, evidence-set selection, and\n",
    "retrieval-control rewards as first-class experimental variables.\n",
    "\n",
    "**Learning outcomes**\n",
    "\n",
    "- compute InfoNCE, pairwise, listwise, distillation, DPO, and policy-gradient objectives;\n",
    "- identify false negatives and label leakage in hard-negative mining;\n",
    "- compare score fusion with rank fusion;\n",
    "- distinguish candidate ranking from budgeted evidence coverage;\n",
    "- attribute relevant evidence lost at retrieval, reranking, or packing.\n",
    "\n",
    "Companion chapters: [Retrieval and ranking](../research/retrieval_and_ranking.md)\n",
    "and [Training and optimization](../research/training_and_optimization.md).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Repository root: resolved from the notebook location\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "import sys\n",
    "\n",
    "ROOT = Path.cwd()\n",
    "if not (ROOT / \"src\").exists():\n",
    "    ROOT = ROOT.parent\n",
    "sys.path.insert(0, str(ROOT / \"src\"))\n",
    "print(\"Repository root: resolved from the notebook location\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Labels define what “relevant” means\n",
    "\n",
    "Positives may be human qrels, answer-containing passages, cited sources,\n",
    "supporting facts, clicked documents, successful tool results, synthetic\n",
    "teacher labels, or passages that improve a downstream reader. These signals\n",
    "disagree. Answer string containment can reward a passage that repeats a\n",
    "false claim; clicks encode position bias; citations may be incomplete;\n",
    "teacher labels inherit model bias; downstream utility can reward spurious\n",
    "shortcuts.\n",
    "\n",
    "Preserve label provenance and uncertainty. Split by source/time/template to\n",
    "prevent leakage. Evaluate retriever recall, reader robustness, and generator\n",
    "parametric knowledge separately.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "temperature 0.25 P(positive) 0.9734 loss 0.027\n",
      "temperature 0.5 P(positive) 0.8532 loss 0.1587\n",
      "temperature 1.0 P(positive) 0.6717 loss 0.3979\n",
      "temperature 2.0 P(positive) 0.5197 loss 0.6545\n",
      "Hinge easy/hard: 0.0 2.5\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.training import contrastive_loss, pairwise_hinge_loss, softmax\n",
    "\n",
    "for temperature in (0.25, 0.5, 1.0, 2.0):\n",
    "    probabilities = softmax((3.0, 2.1, 0.5), temperature)\n",
    "    loss = contrastive_loss(3.0, (2.1, 0.5), temperature)\n",
    "    print(\"temperature\", temperature, \"P(positive)\", round(probabilities[0], 4), \"loss\", round(loss, 4))\n",
    "print(\"Hinge easy/hard:\", pairwise_hinge_loss(3.0, 1.0), pairwise_hinge_loss(1.0, 2.5))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Contrastive learning is largely a negative-sampling design\n",
    "\n",
    "For query (q_i), positive (d_i^+), negatives (d_j^-), a common loss is\n",
    "\n",
    "\\[\n",
    "-\\log\\frac{\\exp(s(q_i,d_i^+)/\\tau)}\n",
    "{\\exp(s(q_i,d_i^+)/\\tau)+\\sum_j\\exp(s(q_i,d_j^-)/\\tau)}.\n",
    "\\]\n",
    "\n",
    "In-batch negatives are cheap but may contain alternate positives. BM25/dense\n",
    "hard negatives teach fine distinctions but can concentrate annotation\n",
    "errors. Cross-encoder mining adds teacher bias; same-source negatives may be\n",
    "genuinely supportive. Track source identity, answer aliases, qrels, and\n",
    "teacher relevance, and quarantine candidates that may be false negatives.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Mask: ((False, False, True), (True, False, True))\n",
      "Unmasked loss: 0.3679\n",
      "False-negative-aware loss: 0.0796\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.training import false_negative_mask, in_batch_contrastive_loss\n",
    "\n",
    "similarities = (\n",
    "    (3.2, 3.0, 0.2),  # document 1 is an unlabeled alternate positive for query 0\n",
    "    (0.1, 3.1, 0.4),\n",
    ")\n",
    "mask = false_negative_mask(({\"doc-0\", \"doc-1\"}, {\"doc-1\"}), (\"doc-0\", \"doc-1\", \"doc-2\"))\n",
    "unmasked = in_batch_contrastive_loss(similarities, positive_indices=(0, 1))\n",
    "masked = in_batch_contrastive_loss(similarities, positive_indices=(0, 1), valid_mask=mask)\n",
    "print(\"Mask:\", mask)\n",
    "print(\"Unmasked loss:\", round(unmasked, 4))\n",
    "print(\"False-negative-aware loss:\", round(masked, 4))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Hard-negative mining needs an audit trail\n",
    "\n",
    "A robust loop retrieves with the current model, joins provenance/qrels,\n",
    "removes known and likely positives, samples across difficulty and source\n",
    "types, trains, and repeats on a frozen evaluation set. Include random/easy\n",
    "negatives so the model retains global separation; include adversarial\n",
    "lexical and semantic confounders; monitor how many mined “negatives” human\n",
    "adjudicators relabel as relevant.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Selected: ['hard-confounder', 'medium-confounder']\n",
      "Quarantined false negatives: ['same-source', 'answer-alias', 'teacher-says-positive']\n",
      "Easy/unselected: ['easy']\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.training import NegativeExample, mine_hard_negatives\n",
    "\n",
    "pool = (\n",
    "    NegativeExample(\"same-source\", 0.99, source_id=\"gold-source\"),\n",
    "    NegativeExample(\"answer-alias\", 0.96, answer_ids=(\"rag\",)),\n",
    "    NegativeExample(\"teacher-says-positive\", 0.91, teacher_relevance=0.8),\n",
    "    NegativeExample(\"hard-confounder\", 0.88),\n",
    "    NegativeExample(\"medium-confounder\", 0.63),\n",
    "    NegativeExample(\"easy\", 0.05),\n",
    ")\n",
    "mining = mine_hard_negatives(\n",
    "    pool, positive_source_ids=(\"gold-source\",), positive_answer_ids=(\"rag\",),\n",
    "    k=2, minimum_score=0.5\n",
    ")\n",
    "print(\"Selected:\", [item.identifier for item in mining.selected])\n",
    "print(\"Quarantined false negatives:\", [item.identifier for item in mining.excluded_false_negatives])\n",
    "print(\"Easy/unselected:\", [item.identifier for item in mining.excluded_easy])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Retriever families learn different representations\n",
    "\n",
    "Dense bi-encoders learn one vector per query/passage (DPR, ANCE, RocketQA,\n",
    "Contriever, GTR, E5, DRAGON). Learned sparse models predict weighted\n",
    "vocabulary dimensions (DeepCT, DeepImpact, SPLADE). Late-interaction models\n",
    "retain token vectors and MaxSim interactions (ColBERT, PLAID, XTR, CITADEL).\n",
    "Reasoning-aware models train on “helpful versus plausible-but-unhelpful”\n",
    "documents. Unified models such as GritLM share embedding and generation.\n",
    "\n",
    "Pretraining choices—masked autoencoding, inverse cloze, synthetic queries,\n",
    "instruction data, domain adaptation, multilingual alignment—change transfer.\n",
    "Report model size, representation bytes, index size, query/document encoding\n",
    "cost, first-stage recall, and downstream utility.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Listwise weak/aligned: 1.2933 1.0133\n",
      "KL weak/aligned: 1.1492 0.0173\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.training import kl_distillation_loss, listwise_cross_entropy\n",
    "\n",
    "teacher = (4.0, 2.0, 1.0, -1.0)\n",
    "weak_student = (1.0, 0.9, 0.8, 0.7)\n",
    "aligned_student = (3.8, 2.1, 1.0, -0.5)\n",
    "relevance = (3.0, 2.0, 1.0, 0.0)\n",
    "print(\"Listwise weak/aligned:\", round(listwise_cross_entropy(weak_student, relevance), 4), round(listwise_cross_entropy(aligned_student, relevance), 4))\n",
    "print(\"KL weak/aligned:\", round(kl_distillation_loss(weak_student, teacher, 2.0), 4), round(kl_distillation_loss(aligned_student, teacher, 2.0), 4))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Query transformation changes recall and can change intent\n",
    "\n",
    "Options include spelling/entity normalization, decomposition, multi-query\n",
    "paraphrases, pseudo-relevance feedback, HyDE hypothetical documents,\n",
    "Query2Doc expansion, step-back abstraction, conversation-history rewriting,\n",
    "metadata/temporal filters, and tool-selected structured queries. Transform\n",
    "quality must be judged against original intent; fluent rewrites can remove a\n",
    "constraint or invent a premise.\n",
    "\n",
    "Run each transform as an ablation and log the original query, every rewrite,\n",
    "retrieved set, new relevant evidence, duplicates, latency, and cost.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sparse: [('grip-2026', 1), ('rag-2020', 2), ('dpr-2020', 3), ('lara-2025', 4), ('search-r1-2025', 5)]\n",
      "Semantic proxy: [('rag-2020', 1), ('fid-2021', 2), ('crag-2024', 3), ('hyde-2022', 4), ('grip-2026', 5)]\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.demo_data import demo_documents\n",
    "from rag_evolution.retrievers import BM25Retriever, HashingSemanticRetriever\n",
    "from rag_evolution.text import chunk_documents\n",
    "\n",
    "chunks = chunk_documents(demo_documents(), chunk_size=85, overlap=10)\n",
    "sparse = BM25Retriever(chunks)\n",
    "dense_proxy = HashingSemanticRetriever(chunks, dimensions=256)\n",
    "query = \"How do DPR and RAG differ in retrieval and generation?\"\n",
    "sparse_results = sparse.search(query, 8)\n",
    "dense_results = dense_proxy.search(query, 8)\n",
    "print(\"Sparse:\", [(item.chunk.document_id, item.rank) for item in sparse_results[:5]])\n",
    "print(\"Semantic proxy:\", [(item.chunk.document_id, item.rank) for item in dense_results[:5]])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Fusion: rank robustness versus score information\n",
    "\n",
    "Reciprocal-rank fusion (RRF) combines ordinal ranks and tolerates\n",
    "incomparable BM25/cosine scales. CombSUM/CombMNZ can exploit score magnitude\n",
    "only after calibration. Learned fusion can use query features and component\n",
    "scores but adds labels and shift risk. Missing candidates, depth, duplicate\n",
    "identities, and weights are part of the definition.\n",
    "\n",
    "A hybrid win does not reveal which component helped. Record per-result raw,\n",
    "calibrated, weighted, and fused scores and compare sparse-only, dense-only,\n",
    "union, RRF, calibrated score fusion, and reranked variants.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "RRF: [('rag-2020', 0.0635), ('grip-2026', 0.0608), ('crag-2024', 0.0581), ('dpr-2020', 0.0573), ('atlas-2022', 0.0548), ('fid-2021', 0.0312)]\n",
      "Calibrated CombSUM: [('rag-2020', 1.882), ('grip-2026', 1.294), ('fid-2021', 0.936), ('dpr-2020', 0.5), ('crag-2024', 0.346), ('lara-2025', 0.329)]\n",
      "Top CombSUM components: {'sparse_calibrated': 0.782026126654701, 'sparse_weighted': 0.782026126654701, 'sparse_raw': 6.002930239470717, 'semantic_calibrated': 1.0, 'semantic_weighted': 1.1, 'semantic_raw': 0.3039839749471921}\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.selection import calibrated_comb_sum, reciprocal_rank_fusion\n",
    "\n",
    "rankings = {\"sparse\": sparse_results, \"semantic\": dense_results}\n",
    "rrf = reciprocal_rank_fusion(rankings, k=6, constant=30)\n",
    "comb = calibrated_comb_sum(rankings, k=6, weights={\"sparse\": 1.0, \"semantic\": 1.1})\n",
    "print(\"RRF:\", [(item.chunk.document_id, round(item.score, 4)) for item in rrf])\n",
    "print(\"Calibrated CombSUM:\", [(item.chunk.document_id, round(item.score, 3)) for item in comb])\n",
    "print(\"Top CombSUM components:\", dict(comb[0].component_scores))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Reranking crosses the query–document boundary\n",
    "\n",
    "Cross-encoders jointly attend to query and candidate and usually improve\n",
    "precision over independent embeddings. MonoT5/RankT5 cast ranking as\n",
    "generation; listwise LLM rerankers compare several candidates; late\n",
    "interaction lies between bi- and cross-encoders. Distill expensive teachers\n",
    "into cheaper rerankers, but validate calibration and position/order effects.\n",
    "\n",
    "First-stage recall remains a hard ceiling. Rerank enough candidates to expose\n",
    "relevant evidence, then report candidate recall, reranked nDCG/recall,\n",
    "latency, truncation, and cross-domain robustness.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Before: ['rag-2020', 'grip-2026', 'crag-2024', 'dpr-2020', 'atlas-2022', 'fid-2021']\n",
      "After: ['rag-2020', 'grip-2026', 'dpr-2020', 'crag-2024', 'atlas-2022', 'lara-2025']\n",
      "Interaction features: {'rerank_retrieval': 1.0, 'rerank_coverage': 0.6, 'rerank_phrase': 0.0, 'rerank_proximity': 0.333, 'rerank_title': 0.4, 'rerank_year': 1.0}\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.rerankers import CrossFeatureReranker\n",
    "\n",
    "candidates = reciprocal_rank_fusion(rankings, k=10, constant=30)\n",
    "reranked = CrossFeatureReranker().rerank(query, candidates, k=6)\n",
    "print(\"Before:\", [item.chunk.document_id for item in candidates[:6]])\n",
    "print(\"After:\", [item.chunk.document_id for item in reranked])\n",
    "print(\"Interaction features:\", {k: round(v, 3) for k, v in reranked[0].component_scores.items() if k.startswith(\"rerank_\")})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. The generator consumes a set, not a leaderboard\n",
    "\n",
    "Top-k can waste a budget on redundant passages while omitting a complementary\n",
    "fact. Evidence selection is a weighted set-cover/knapsack problem over\n",
    "claims, entities, sources, time versions, and token cost. Diversity/MMR is a\n",
    "useful proxy; explicit claim coverage is better when support annotations are\n",
    "available. Authority and conflict cannot be reduced to similarity alone.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Budget: 126 spent: 126\n",
      "Selected: ['rag-2020', 'dpr-2020']\n",
      "Covered/uncovered: ('generator', 'retriever', 'training') ('fusion',)\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.selection import SelectionCandidate, greedy_budgeted_coverage\n",
    "from rag_evolution.text import tokenize\n",
    "\n",
    "supports = {\n",
    "    \"dpr-2020\": (\"retriever\", \"training\"),\n",
    "    \"rag-2020\": (\"retriever\", \"generator\"),\n",
    "    \"fid-2020\": (\"generator\", \"fusion\"),\n",
    "}\n",
    "coverage_candidates = []\n",
    "for item in reranked:\n",
    "    claims = supports.get(item.chunk.document_id, ())\n",
    "    if claims:\n",
    "        coverage_candidates.append(\n",
    "            SelectionCandidate(item, claims, max(1, len(tokenize(item.chunk.text))))\n",
    "        )\n",
    "budget = sum(sorted(candidate.cost for candidate in coverage_candidates)[:2])\n",
    "selection = greedy_budgeted_coverage(\n",
    "    coverage_candidates,\n",
    "    required=(\"retriever\", \"training\", \"generator\", \"fusion\"),\n",
    "    budget=budget,\n",
    "    relevance_weight=0.02,\n",
    ")\n",
    "print(\"Budget:\", budget, \"spent:\", selection.spent)\n",
    "print(\"Selected:\", [item.result.chunk.document_id for item in selection.selected])\n",
    "print(\"Covered/uncovered:\", selection.covered, selection.uncovered)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. Preference and RL objectives need guarded rewards\n",
    "\n",
    "DPO can prefer cited, concise, abstaining, or low-cost trajectories relative\n",
    "to a reference policy. REINFORCE/GRPO/PPO-style optimization can learn\n",
    "retrieve/query/stop actions. Outcome-only answer rewards permit fabricated\n",
    "evidence, spurious search, or formatting hacks. Process rewards (support,\n",
    "information gain, redundancy, valid tool calls, calibrated stopping) help but\n",
    "are themselves gameable.\n",
    "\n",
    "Keep hard security/cost limits outside the learned policy. Audit reward\n",
    "correlation with human judgments, search traces, fabricated citations,\n",
    "over/under-search, transfer across corpora, and performance when retriever or\n",
    "generator changes.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "DPO preferred/reversed: 0.513 0.913\n",
      "Returns: (0.865, 0.85, 1.0)\n",
      "Advantages: (0.665, 0.65, 0.8) loss 0.2282\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.training import dpo_loss, reinforce_loss\n",
    "\n",
    "preferred = dpo_loss(-1.0, -3.0, -2.0, -2.0, beta=0.2)\n",
    "reversed_pair = dpo_loss(-3.0, -1.0, -2.0, -2.0, beta=0.2)\n",
    "trajectory = reinforce_loss(\n",
    "    action_log_probabilities=(-0.3, -0.5, -0.2),\n",
    "    rewards=(0.1, -0.05, 1.0),\n",
    "    baseline=(0.2, 0.2, 0.2),\n",
    "    discount=0.9,\n",
    ")\n",
    "print(\"DPO preferred/reversed:\", round(preferred, 4), round(reversed_pair, 4))\n",
    "print(\"Returns:\", tuple(round(value, 3) for value in trajectory.returns))\n",
    "print(\"Advantages:\", tuple(round(value, 3) for value in trajectory.advantages), \"loss\", round(trajectory.loss, 4))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10. Attribute loss across the evidence pipeline\n",
    "\n",
    "Retrieval recall asks whether relevant evidence entered the candidate pool.\n",
    "Rerank survival asks whether it remained after second-stage selection. Pack\n",
    "survival asks whether it reached the model after deduplication and budgets.\n",
    "Context utilization asks whether the answer actually used it. Citation\n",
    "entailment/completeness ask whether claims point to supporting spans. One\n",
    "end-to-end score hides these failure locations.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Recall/survival: {'retrieval': 1.0, 'rerank': 1.0, 'pack': 0.5, 'end_to_end': 0.5}\n",
      "Lost at stages: () () ('dpr-2020',)\n"
     ]
    }
   ],
   "source": [
    "from rag_evolution.context import ContextPacker\n",
    "from rag_evolution.selection import evidence_flow\n",
    "\n",
    "packed = ContextPacker(max_tokens=180, max_chunks=3).pack(reranked)\n",
    "flow = evidence_flow((\"dpr-2020\", \"rag-2020\"), candidates, reranked, packed)\n",
    "print(\"Recall/survival:\", {\n",
    "    \"retrieval\": round(flow.retrieval_recall, 3),\n",
    "    \"rerank\": round(flow.rerank_survival, 3),\n",
    "    \"pack\": round(flow.pack_survival, 3),\n",
    "    \"end_to_end\": round(flow.end_to_end_recall, 3),\n",
    "})\n",
    "print(\"Lost at stages:\", flow.lost_at_retrieval, flow.lost_at_rerank, flow.lost_at_pack)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Experiment checklist\n",
    "\n",
    "Freeze corpus/qrels; record query and document encoders, prefixes, negatives,\n",
    "temperatures, mining checkpoint, fusion calibration, candidate depth,\n",
    "reranker truncation, pack budget, and seeds. Report per-query outputs and\n",
    "slices with paired confidence intervals. Evaluate BM25, dense, learned\n",
    "sparse, hybrid, reranked, oracle-context, and closed-book controls.\n",
    "\n",
    "**This lab does not reproduce** billion-parameter training or claim its\n",
    "hashed semantic proxy is neural retrieval. It makes objective functions and\n",
    "component boundaries executable so a real model can be substituted without\n",
    "changing the audit.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.9"
  },
  "rag_evolution": {
   "presentation": "plain-field-notebook",
   "visual_version": 2
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
