Dynamic pricing extracts margin by exploiting the gap between what a platform knows and what a buyer knows. A user who browses a hotel across several sessions signals intent; the platform raises the price accordingly. That information asymmetry — the Cost of Information — is the economic engine behind session-based pricing in travel, hospitality, and e-commerce.
@@ -355,37 +357,106 @@
-
+
+
+
-
-
How it works
-
- The methodology runs in three stages: observe, distinguish, defend.
-
-
-
Stage 1 — Observe
-
- Both human participants and LLM agents are assigned goal-driven tasks on a live instrumented storefront (hotel or airline mode). Every interaction is logged as a timestamped event tuple (action, item, timestamp). Actions are partitioned into four semantic categories — cart, dwell, navigation, filter — with decreasing signal weights (4.0, 2.0, 1.0, 0.5) calibrated by the KL divergence between human and agent transition rows. Price quotes are streamed to a separate Kafka topic, enabling joint analysis of behavior and pricing exposure. The platform runs a surge-discount heuristic during collection to expose participants to state-dependent prices.
-
-
-
Stage 2 — Distinguish
-
- From the labeled session trajectories, we estimate class-specific Markov transition kernels T̂H and T̂A by maximum likelihood. For any new partial trajectory τ', we compute KL divergence to each prototype:
-
-
- ΔH = DKL (T̂' ∥ T̄H ), ΔA = DKL (T̂' ∥ T̄A )
-
-
- The gap score g (τ') = ΔH − ΔA maps to a weak agent probability via a temperature-controlled logistic function: f (τ') = σ((ΔH − ΔA ) / T). This is a continuous signal, not a binary bot flag. The Mann–Whitney test on gap scores between the 13-human and 16-agent cohorts yields U = 2.0, p = 0.0006 — the behavioral distributions are well separated.
-
-
-
Stage 3 — Defend
-
- A contamination generator G(α) mixes real human trajectories with synthetic agent trajectories drawn from T̂A to produce training distributions at any contamination level α ∈ [0, 1]. The pricing policy is trained as a Stackelberg leader against a Wasserstein ambiguity set around the generator's empirical distribution, minimizing worst-case regret over plausible demand shifts. The per-step reward penalizes COI leakage — weighted by f (τ') — while a UX index bounds harm to legitimate users. Sweeps ran across 384 TPU chips (v4, v5e, v6e Trillium) covering six contamination levels and multiple algorithm variants (PPO, A2C, DQN, Q-table).
-
+
New interaction environment of future commerce .
+
+
+
+
+
+
Users
+
Have new needs and means of research & acquisition.
+
+
+
+
+
+
Agents
+
Use browsers (C/BUA) to look human and create clean sessions .
+
+
+
+
Platforms
+
Run standard pricing algorithms and experience revenue loss.
+
+
+
+
+
+
+
+
When agents can repeatedly query prices, realizable markup disappears .
+
+
COI = E [P] − p
+
Cost of Information — the expected premium dynamic pricing earns over the reservation price — collapses to zero as the number of independent querying agents grows.
+
+
+
+
+
+
+
+
+
We study behavior, convert it into a control signal, and train a pricing policy that survives contamination.
+
+
+ 01
+ Observe
+ Human participants and LLM agents complete goal-driven hotel and airline tasks. The storefront records behavior events and price quotes as timestamped trajectories.
+
+
+ 02
+ Distinguish
+ Session paths become transition kernels. KL distance to human and agent prototypes yields a continuous agent-probability signal.
+
+
+ 03
+ Defend
+ A contamination generator mixes human and synthetic agent trajectories. A distributionally robust RL policy optimizes price under worst-case demand shifts.
+
+
+
+
+
+
+
+
+
+
Agents distort marketplace signals . PHANTOM uses behavioral distinguishability and DR–RL to preserve pricing power .
+
+
+ 01
+ We can distinguish humans from agents at the transition-kernel level.Mann–Whitney U = 2.0, p = 0.0006 across 29 labeled sessions.
+
+
+ 02
+ Revenue declines monotonically in agent-contaminated systems.Each 1.0 step of contamination α removes ~90,140 in cohort revenue (p < 10-77 ).
+
+
+ 03
+ Distributionally robust RL preserves margin under worst-case contamination.Defended policy holds positive COI gap over baseline across α ∈ [0, 1].
+
+
+
Our solution can be forward-deployed to any e-commerce platform to preserve their COI .
+
+
+
+
@@ -428,35 +499,7 @@
-
-
-
-
-
Defense Scenes
-
-
-
-
-
-
COI from first principles.
-
-
-
-
-
-
Behavioral kernel construction: learning how humans and agents differ.
-
-
-
-
-
-
Distributionally robust control loop.
-
-
-
-
-
-
+
@@ -467,7 +510,7 @@
-
Full Thesis
+
Full thesis.
diff --git a/docs/static/css/defense-theme.css b/docs/static/css/defense-theme.css
new file mode 100644
index 0000000..6bc4eca
--- /dev/null
+++ b/docs/static/css/defense-theme.css
@@ -0,0 +1,740 @@
+:root {
+ --phantom-bg: #eef3f7;
+ --phantom-paper: rgba(255, 255, 255, 0.78);
+ --phantom-paper-solid: #ffffff;
+ --phantom-ink: #1f2a38;
+ --phantom-muted: #59636e;
+ --phantom-faint: #dce5eb;
+ --phantom-line: rgba(31, 42, 56, 0.13);
+ --phantom-teal: #28aaa5;
+ --phantom-teal-dark: #16837f;
+ --phantom-blue: #527dad;
+ --phantom-blue-soft: rgba(82, 125, 173, 0.18);
+ --phantom-shadow: 0 28px 80px rgba(31, 42, 56, 0.12);
+ --phantom-soft-shadow: 0 14px 45px rgba(31, 42, 56, 0.08);
+}
+
+html {
+ scroll-behavior: smooth;
+ background: var(--phantom-bg);
+}
+
+body {
+ color: var(--phantom-ink);
+ background:
+ radial-gradient(70rem 22rem at 72% 8%, rgba(31, 42, 56, 0.15), transparent 58%),
+ radial-gradient(54rem 24rem at 20% 62%, rgba(31, 42, 56, 0.12), transparent 62%),
+ linear-gradient(180deg, #f7fafc 0%, var(--phantom-bg) 48%, #f8fafb 100%);
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ letter-spacing: -0.02em;
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ inset: -20vh -10vw auto -10vw;
+ height: 72vh;
+ pointer-events: none;
+ z-index: -1;
+ opacity: 0.88;
+ filter: blur(30px);
+ background:
+ radial-gradient(45rem 16rem at 9% 34%, rgba(18, 23, 31, 0.11), transparent 62%),
+ radial-gradient(35rem 11rem at 65% 24%, rgba(18, 23, 31, 0.13), transparent 65%),
+ radial-gradient(42rem 17rem at 45% 88%, rgba(82, 125, 173, 0.13), transparent 68%);
+}
+
+strong, b {
+ font-weight: 700;
+ color: inherit;
+}
+
+a {
+ color: var(--phantom-blue);
+ text-decoration-thickness: 0.08em;
+ text-underline-offset: 0.18em;
+}
+
+main {
+ overflow: hidden;
+}
+
+.container.is-max-desktop {
+ max-width: 1180px !important;
+}
+
+.section,
+.hero-body {
+ position: relative;
+}
+
+.section {
+ padding: 5rem 1.5rem;
+}
+
+.title,
+.subtitle,
+h1,
+h2,
+h3,
+h4,
+button,
+.button {
+ /* important needed to beat index.css serif heading rule */
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace !important;
+ letter-spacing: -0.04em;
+}
+
+/* Defense cover */
+.defense-cover {
+ min-height: 92vh;
+ padding: 2rem 0 3.5rem;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(238, 243, 247, 0.92));
+}
+
+.defense-cover::after {
+ content: "";
+ position: absolute;
+ right: -10vw;
+ bottom: -13rem;
+ width: 64vw;
+ height: 26rem;
+ background: rgba(31, 42, 56, 0.12);
+ filter: blur(38px);
+ border-radius: 65% 35% 47% 53%;
+ transform: rotate(-8deg);
+ pointer-events: none;
+}
+
+.defense-cover .hero-body {
+ padding: 5.5rem 1.5rem 4rem;
+}
+
+.defense-hero-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.08fr) minmax(280px, 0.72fr);
+ gap: clamp(2rem, 6vw, 7rem);
+ align-items: center;
+}
+
+.defense-kicker,
+.defense-meta-row,
+.defense-chip-row,
+.defense-mini-label,
+.tpu-credit {
+ color: rgba(31, 42, 56, 0.66);
+ font-size: 0.88rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.defense-kicker {
+ margin-bottom: 1.2rem;
+}
+
+.publication-title.defense-title {
+ margin: 0;
+ color: var(--phantom-ink);
+ font-size: clamp(4.25rem, 12vw, 10rem);
+ line-height: 0.82;
+ font-weight: 800;
+ letter-spacing: -0.09em;
+ text-transform: uppercase;
+}
+
+.defense-subtitle {
+ max-width: 950px;
+ margin: clamp(1.5rem, 3vw, 2.3rem) 0 0;
+ color: var(--phantom-ink);
+ font-size: clamp(1.55rem, 3.2vw, 3.35rem);
+ line-height: 1.35;
+ font-weight: 400;
+ letter-spacing: 0.05em;
+}
+
+.mark,
+mark,
+.defense-highlight {
+ background: linear-gradient(0deg, var(--phantom-blue) 0%, var(--phantom-blue) 100%);
+ color: #ffffff;
+ padding: 0 0.1em;
+ line-height: inherit;
+ box-decoration-break: clone;
+ -webkit-box-decoration-break: clone;
+}
+
+.defense-chip-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.7rem 0.9rem;
+ margin-top: 2.15rem;
+ text-transform: none;
+ letter-spacing: -0.02em;
+ font-size: clamp(0.92rem, 1.4vw, 1.2rem);
+}
+
+.defense-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.65rem;
+ color: rgba(31, 42, 56, 0.70);
+}
+
+.defense-chip::before {
+ content: "";
+ width: 0.42rem;
+ height: 0.42rem;
+ border-radius: 999px;
+ background: var(--phantom-blue);
+ box-shadow: 0 0 0 0.35rem var(--phantom-blue-soft);
+}
+
+.defense-meta-card {
+ margin-top: 2.4rem;
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 0.6rem 1rem;
+ align-items: center;
+ padding: 0.85rem 1rem;
+ border: 1px solid var(--phantom-line);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.58);
+ backdrop-filter: blur(18px);
+ box-shadow: var(--phantom-soft-shadow);
+ color: rgba(31, 42, 56, 0.74);
+ font-size: 0.95rem;
+}
+
+.defense-meta-card .dot {
+ width: 0.26rem;
+ height: 0.26rem;
+ border-radius: 50%;
+ background: var(--phantom-teal);
+}
+
+.defense-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 1.8rem;
+}
+
+.defense-links .button,
+.publication-links .button {
+ border: 1px solid rgba(31, 42, 56, 0.18) !important;
+ background: rgba(31, 42, 56, 0.92) !important;
+ color: #ffffff !important;
+ box-shadow: 0 12px 30px rgba(31, 42, 56, 0.16);
+ transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease;
+}
+
+.defense-links .button:hover,
+.publication-links .button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 16px 40px rgba(31, 42, 56, 0.20);
+ background: var(--phantom-ink) !important;
+}
+
+.defense-links .button.is-light-outline {
+ background: rgba(255, 255, 255, 0.72) !important;
+ color: var(--phantom-ink) !important;
+}
+
+.tpu-credit {
+ margin-top: 1.35rem;
+ text-transform: none;
+ letter-spacing: 0.02em;
+}
+
+.tpu-credit .accent {
+ color: var(--phantom-blue);
+ font-weight: 700;
+}
+
+.defense-visual {
+ justify-self: end;
+ width: min(100%, 430px);
+}
+
+.defense-orbit-card {
+ position: relative;
+ min-height: 435px;
+ border: 1px solid var(--phantom-line);
+ border-radius: 2rem;
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.84), rgba(238, 243, 247, 0.68));
+ box-shadow: var(--phantom-shadow);
+ overflow: hidden;
+}
+
+.defense-orbit-card::before {
+ content: "";
+ position: absolute;
+ inset: 2rem;
+ border: 1px dashed rgba(31, 42, 56, 0.22);
+ border-radius: 44% 56% 58% 42%;
+ transform: rotate(-14deg);
+}
+
+.defense-orbit-card::after {
+ content: "";
+ position: absolute;
+ right: -4rem;
+ bottom: -5rem;
+ width: 18rem;
+ height: 12rem;
+ background: rgba(40, 170, 165, 0.14);
+ border-radius: 50%;
+ filter: blur(18px);
+}
+
+.defense-art-stack {
+ position: relative;
+ display: grid;
+ min-height: 435px;
+ place-items: center;
+ z-index: 1;
+}
+
+.defense-art-stack .agent-art {
+ width: min(70%, 250px);
+ transform: translateY(-12px);
+ filter: drop-shadow(0 28px 28px rgba(31, 42, 56, 0.12));
+}
+
+.defense-art-stack .mini-token {
+ position: absolute;
+ width: 4.8rem;
+ height: 4.8rem;
+ display: grid;
+ place-items: center;
+ border-radius: 1.3rem;
+ border: 1px solid rgba(82, 125, 173, 0.28);
+ background: rgba(255, 255, 255, 0.72);
+ color: var(--phantom-blue);
+ box-shadow: var(--phantom-soft-shadow);
+ font-size: 1.35rem;
+}
+
+.defense-art-stack .mini-token:nth-child(2) { top: 3.1rem; right: 3.2rem; }
+.defense-art-stack .mini-token:nth-child(3) { left: 2.8rem; bottom: 6.1rem; color: var(--phantom-teal-dark); }
+.defense-art-stack .mini-token:nth-child(4) { right: 5.6rem; bottom: 3.2rem; color: var(--phantom-ink); }
+
+/* Defense overview strip */
+.defense-overview-strip {
+ margin-top: -3.8rem;
+ padding: 0 1.5rem 4.6rem;
+ position: relative;
+ z-index: 2;
+}
+
+.defense-overview-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+}
+
+.defense-overview-card,
+.actor-card,
+.defense-step,
+.hf-callout,
+.coi-equation,
+pre,
+.publication-banner {
+ border: 1px solid var(--phantom-line);
+ background: var(--phantom-paper);
+ box-shadow: var(--phantom-soft-shadow);
+ backdrop-filter: blur(18px);
+}
+
+.defense-overview-card {
+ min-height: 9rem;
+ padding: 1.3rem;
+ border-radius: 1.4rem;
+}
+
+.defense-overview-card .num {
+ color: var(--phantom-blue);
+ font-weight: 700;
+ font-size: 0.85rem;
+}
+
+.defense-overview-card h3 {
+ margin: 1rem 0 0.45rem;
+ color: var(--phantom-ink);
+ font-size: clamp(1.15rem, 2vw, 1.65rem);
+ line-height: 1.05;
+}
+
+.defense-overview-card p {
+ color: var(--phantom-muted);
+ font-size: 0.95rem;
+ line-height: 1.45;
+}
+
+/* Main sections */
+.hero.teaser,
+.hero.is-small,
+.hero.is-small.is-light,
+.hero.is-light,
+.section.hero.is-light,
+.defense-block {
+ background: transparent !important;
+}
+
+.publication-banner {
+ padding: 1rem;
+ border-radius: 1.5rem;
+ overflow: hidden;
+}
+
+.publication-banner img,
+.actor-art img {
+ filter: drop-shadow(0 18px 22px rgba(31, 42, 56, 0.10));
+}
+
+.defense-heading,
+.title.is-3,
+.content h2.title {
+ color: var(--phantom-ink) !important;
+ font-size: clamp(2rem, 4.5vw, 4.8rem) !important;
+ /* enough leading that .mark backgrounds on wrapped lines don't overlap adjacent text */
+ line-height: 1.2 !important;
+ font-weight: 700 !important;
+ letter-spacing: -0.06em !important;
+ text-align: left !important;
+ margin-bottom: 2rem !important;
+}
+
+.title.is-4,
+.content h3.title {
+ color: var(--phantom-ink) !important;
+ font-size: clamp(1.35rem, 2.2vw, 2rem) !important;
+ font-weight: 700 !important;
+ letter-spacing: -0.05em !important;
+ margin-top: 2rem !important;
+}
+
+.content {
+ color: var(--phantom-muted);
+ font-size: 1.02rem;
+ line-height: 1.68;
+}
+
+.content.has-text-justified,
+.content.has-text-justified p {
+ text-align: left !important;
+}
+
+.content p + p {
+ margin-top: 1.05rem;
+}
+
+.defense-block,
+.section.hero.is-light {
+ position: relative;
+}
+
+.defense-block::before,
+.section.hero.is-light::before {
+ content: "";
+ position: absolute;
+ inset: 1rem 0 auto 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(31, 42, 56, 0.13), transparent);
+}
+
+.defense-block .defense-heading {
+ margin-bottom: 3.5rem !important;
+}
+
+.actor-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1.1rem;
+ position: relative;
+ z-index: 1;
+}
+
+.actor-card {
+ display: flex;
+ flex-direction: column;
+ min-height: 25rem;
+ padding: 1.4rem;
+ border-radius: 1.5rem;
+}
+
+.actor-card h3 {
+ margin-top: 1.1rem;
+ color: var(--phantom-ink);
+ font-size: clamp(1.6rem, 3vw, 2.55rem);
+ line-height: 1;
+ font-weight: 700;
+}
+
+.actor-card p {
+ margin-top: 1rem;
+ color: var(--phantom-muted);
+ font-size: 1rem;
+ line-height: 1.45;
+}
+
+.actor-art {
+ min-height: 12rem;
+ display: grid;
+ place-items: center;
+}
+
+.actor-art img {
+ max-height: 10.5rem;
+ width: auto;
+}
+
+.actor-icon {
+ display: grid;
+ width: 8.75rem;
+ height: 8.75rem;
+ place-items: center;
+ border: 2px solid rgba(82, 125, 173, 0.55);
+ border-radius: 1.7rem;
+ background: linear-gradient(145deg, rgba(40, 170, 165, 0.20), rgba(255, 255, 255, 0.94));
+ color: var(--phantom-teal-dark);
+ font-size: 3.6rem;
+ transform: rotate(-6deg);
+}
+
+.underline {
+ text-decoration: underline;
+ text-decoration-thickness: 0.09em;
+ text-underline-offset: 0.13em;
+}
+
+.coi-equation {
+ border-radius: 1.7rem;
+ padding: clamp(1.6rem, 4vw, 3rem);
+}
+
+.coi-equation .formula {
+ color: #111111;
+ font-family: Georgia, "Times New Roman", serif;
+ font-size: clamp(3rem, 9vw, 7.6rem);
+ line-height: 1;
+ letter-spacing: -0.07em;
+}
+
+.coi-equation .caption {
+ max-width: 780px;
+ margin-top: 1.3rem;
+ color: var(--phantom-muted);
+ font-size: 1.05rem;
+}
+
+.defense-method-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ margin-top: 2rem;
+}
+
+.defense-step {
+ border-radius: 1.4rem;
+ padding: 1.35rem;
+}
+
+.defense-step .step-num {
+ display: inline-grid;
+ place-items: center;
+ width: 2.6rem;
+ height: 2.6rem;
+ margin-bottom: 1rem;
+ border: 1px solid rgba(40, 170, 165, 0.38);
+ border-radius: 50%;
+ color: var(--phantom-teal-dark);
+ background: rgba(40, 170, 165, 0.10);
+ font-weight: 700;
+}
+
+.defense-step h3 {
+ margin: 0 0 0.55rem !important;
+ font-size: 1.45rem !important;
+}
+
+.defense-step p {
+ color: var(--phantom-muted);
+ font-size: 0.95rem;
+ line-height: 1.5;
+}
+
+.takeaways {
+ list-style: none;
+ margin: 2rem 0 0 !important;
+ padding: 0;
+ display: grid;
+ gap: 0.9rem;
+}
+
+.takeaways li {
+ display: grid;
+ grid-template-columns: 5rem minmax(0, 1fr);
+ gap: 1rem;
+ align-items: start;
+ padding: 1.2rem 1.3rem;
+ border: 1px solid var(--phantom-line);
+ border-radius: 1.2rem;
+ background: rgba(255, 255, 255, 0.68);
+}
+
+.takeaways .num {
+ color: var(--phantom-blue);
+ font-size: 1.4rem;
+ font-weight: 700;
+}
+
+.takeaways .stat {
+ display: block;
+ margin-top: 0.45rem;
+ color: var(--phantom-muted);
+ font-size: 0.9rem;
+}
+
+.deploy-line {
+ margin: 2rem 0 0;
+ color: var(--phantom-ink);
+ font-size: clamp(1.45rem, 3vw, 2.5rem);
+ line-height: 1.25;
+ font-weight: 700;
+}
+
+.deploy-line strong {
+ background: var(--phantom-blue);
+ color: #ffffff;
+ padding: 0 0.1em;
+ box-decoration-break: clone;
+ -webkit-box-decoration-break: clone;
+}
+
+.hf-callout {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 1rem;
+ margin-top: 1.6rem;
+ padding: 1.2rem;
+ border-radius: 1.3rem;
+}
+
+.hf-emoji {
+ width: 3.6rem;
+ height: 3.6rem;
+ display: grid;
+ place-items: center;
+ border-radius: 1rem;
+ background: rgba(255, 215, 0, 0.20);
+ font-size: 1.9rem;
+}
+
+.hf-callout h4 {
+ margin: 0 0 0.3rem;
+ color: var(--phantom-ink);
+ font-size: 1.12rem;
+}
+
+.hf-callout p {
+ margin: 0 0 0.35rem;
+ color: var(--phantom-muted);
+}
+
+pre#bibtex-code,
+pre {
+ border-radius: 1.2rem;
+ color: var(--phantom-ink);
+}
+
+.footer {
+ background: rgba(255, 255, 255, 0.54);
+ border-top: 1px solid var(--phantom-line);
+ color: var(--phantom-muted);
+}
+
+.more-works-container,
+.scroll-to-top {
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+}
+
+.more-works-btn,
+.scroll-to-top {
+ background: rgba(255, 255, 255, 0.78) !important;
+ color: var(--phantom-ink) !important;
+ border: 1px solid var(--phantom-line) !important;
+ box-shadow: var(--phantom-soft-shadow) !important;
+ backdrop-filter: blur(16px);
+}
+
+.more-works-dropdown {
+ border: 1px solid var(--phantom-line) !important;
+ border-radius: 1.2rem !important;
+ box-shadow: var(--phantom-shadow) !important;
+}
+
+@media (max-width: 900px) {
+ .defense-hero-grid,
+ .defense-overview-grid,
+ .actor-grid,
+ .defense-method-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .defense-cover .hero-body {
+ padding-top: 4rem;
+ }
+
+ .defense-visual {
+ justify-self: stretch;
+ }
+
+ .defense-orbit-card,
+ .defense-art-stack {
+ min-height: 330px;
+ }
+
+ .defense-art-stack .agent-art {
+ width: min(56%, 205px);
+ }
+
+ .defense-meta-card {
+ border-radius: 1.2rem;
+ }
+
+ .publication-title.defense-title {
+ font-size: clamp(4rem, 18vw, 6.2rem);
+ }
+}
+
+@media (max-width: 560px) {
+ .section {
+ padding: 3.6rem 1.1rem;
+ }
+
+ .defense-cover .hero-body {
+ padding-left: 1.1rem;
+ padding-right: 1.1rem;
+ }
+
+ .defense-subtitle {
+ letter-spacing: 0.02em;
+ }
+
+ .takeaways li,
+ .hf-callout {
+ grid-template-columns: 1fr;
+ }
+
+ .defense-chip-row,
+ .defense-links {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .defense-links .button {
+ justify-content: center;
+ }
+}
diff --git a/docs/static/css/index.css b/docs/static/css/index.css
index e14e075..9718b15 100644
--- a/docs/static/css/index.css
+++ b/docs/static/css/index.css
@@ -31,6 +31,15 @@
--paper-ink-strong: #303030;
--paper-ink-soft: #6b6b6b;
--paper-accent: #2f2f2f;
+ --defense-bg: #f4f7f9;
+ --defense-surface: #ffffff;
+ --defense-teal: #5fb3a8;
+ --defense-teal-soft: #b9e2dc;
+ --defense-teal-bg: #c8ebe5;
+ --defense-blue: #2d6f9f;
+ --defense-ink: #1d2630;
+ --defense-ink-soft: #5a6470;
+ --mono-stack: "IBM Plex Mono", "JetBrains Mono", "Roboto Mono", "SF Mono", Menlo, Consolas, monospace;
--radius: 10px;
--radius-lg: 14px;
}
diff --git a/docs/static/images/agent.svg b/docs/static/images/agent.svg
new file mode 100644
index 0000000..b634a4b
--- /dev/null
+++ b/docs/static/images/agent.svg
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/static/images/apple-touch-icon.png b/docs/static/images/apple-touch-icon.png
new file mode 100644
index 0000000..43dc6b0
Binary files /dev/null and b/docs/static/images/apple-touch-icon.png differ
diff --git a/docs/static/images/banner.svg b/docs/static/images/banner.svg
index 7a6400e..e4e51fb 100644
--- a/docs/static/images/banner.svg
+++ b/docs/static/images/banner.svg
@@ -56,7 +56,7 @@
Why COI Erodes with Agent Saturation
p(1) = min(p 1 , ..., p N )
- P(p(1) > t) = [1 - F(t)] N
+ P(p(1) > t) = [1 - F(t)] N
@@ -129,9 +129,9 @@
Kernel shape is the compact behavioral signature used downstream.
-
+
- Separability into a Control Signal
+ Distinguishability into a Control Signal
ΔH = D KL (T̂' || T̄ H )
ΔA = D KL (T̂' || T̄ A )
g = ΔH - Δ A
diff --git a/docs/static/images/favicon.ico b/docs/static/images/favicon.ico
index 101a827..58c0afd 100644
Binary files a/docs/static/images/favicon.ico and b/docs/static/images/favicon.ico differ
diff --git a/docs/static/images/human.svg b/docs/static/images/human.svg
new file mode 100644
index 0000000..d0335d2
--- /dev/null
+++ b/docs/static/images/human.svg
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/paper/defense/NARRATIVE.md b/paper/defense/NARRATIVE.md
new file mode 100644
index 0000000..7203bcd
--- /dev/null
+++ b/paper/defense/NARRATIVE.md
@@ -0,0 +1,51 @@
+---
+present_time: 15 minutes
+qa: 15 minutes
+---
+
+> Notes for presentation deck: keep minimal text, highlight only key metrics or keywords and diagrams, if possible do progressive reveal of items on slides, if going through a list, make each appear progressively on new slides like an animation.
+
+# Introduction [2min]
+> Hook: Extracting margin in markets with high density of AI agents.
+- Say what today's agenda is (show in the blocks at the botton of each slide and with each slide indicate which stage we are at)
+- Highlight problem (add financial consequence)
+ - What are we trying to answer?
+
+# First Stage (Platform Development) [4min]
+- Talk about designing the platform (nextjs design and apache airflow and kafka)
+
+## About the Platform
+- Show an architecture diagram.
+
+## Dataset Brief
+- Screenshot of the HF dataset and highlight some key features of the dataset with big numbers indicated.
+
+## Experimental Design
+- Say how we collected data and how we used AI Agents
+
+### AI Agents
+- browser use
+- models used (say we used the LLM router for different models)
+
+# Second Stage (Distinguishability Construction) [4min]
+- Explain kernels of behavior (what is a kernel)
+- How we separate kernels and finally how we turn that into a probability.
+
+# DR-RL [4min]
+- Explain simple wesserstein balls and ambiguity
+- Highlight computational complexity
+
+## Results [1min]
+- Empirical results from experiments
+
+# Conclusions
+- Consequences of our work (financial and future implications for pricing systems)
+- Did we answer what we wanted? How?
+
+# Appendix
+
+## Derivation of the COI theorem
+## Reward Structure Composition
+## On our Sample Size
+
+
diff --git a/paper/defense/defense.nav b/paper/defense/defense.nav
new file mode 100644
index 0000000..d39196d
--- /dev/null
+++ b/paper/defense/defense.nav
@@ -0,0 +1,137 @@
+\headcommand {\slideentry {0}{0}{1}{1/1}{}{0}}
+\headcommand {\beamer@framepages {1}{1}}
+\headcommand {\slideentry {0}{0}{2}{2/2}{}{0}}
+\headcommand {\beamer@framepages {2}{2}}
+\headcommand {\slideentry {0}{0}{3}{3/3}{}{0}}
+\headcommand {\beamer@framepages {3}{3}}
+\headcommand {\slideentry {0}{0}{4}{4/4}{}{0}}
+\headcommand {\beamer@framepages {4}{4}}
+\headcommand {\slideentry {0}{0}{5}{5/5}{}{0}}
+\headcommand {\beamer@framepages {5}{5}}
+\headcommand {\slideentry {0}{0}{6}{6/6}{}{0}}
+\headcommand {\beamer@framepages {6}{6}}
+\headcommand {\slideentry {0}{0}{7}{7/7}{}{0}}
+\headcommand {\beamer@framepages {7}{7}}
+\headcommand {\slideentry {0}{0}{8}{8/8}{}{0}}
+\headcommand {\beamer@framepages {8}{8}}
+\headcommand {\slideentry {0}{0}{9}{9/9}{}{0}}
+\headcommand {\beamer@framepages {9}{9}}
+\headcommand {\slideentry {0}{0}{10}{10/10}{}{0}}
+\headcommand {\beamer@framepages {10}{10}}
+\headcommand {\beamer@sectionpages {1}{10}}
+\headcommand {\beamer@subsectionpages {1}{10}}
+\headcommand {\sectionentry {1}{Platform Development}{11}{Platform Development}{0}}
+\headcommand {\slideentry {1}{0}{1}{11/11}{}{0}}
+\headcommand {\beamer@framepages {11}{11}}
+\headcommand {\slideentry {1}{0}{2}{12/12}{}{0}}
+\headcommand {\beamer@framepages {12}{12}}
+\headcommand {\slideentry {1}{0}{3}{13/13}{}{0}}
+\headcommand {\beamer@framepages {13}{13}}
+\headcommand {\beamer@sectionpages {11}{13}}
+\headcommand {\beamer@subsectionpages {11}{13}}
+\headcommand {\sectionentry {2}{Distinguishability Construction}{14}{Distinguishability Construction}{0}}
+\headcommand {\slideentry {2}{0}{1}{14/14}{}{0}}
+\headcommand {\beamer@framepages {14}{14}}
+\headcommand {\slideentry {2}{0}{2}{15/15}{}{0}}
+\headcommand {\beamer@framepages {15}{15}}
+\headcommand {\slideentry {2}{0}{3}{16/16}{}{0}}
+\headcommand {\beamer@framepages {16}{16}}
+\headcommand {\beamer@sectionpages {14}{16}}
+\headcommand {\beamer@subsectionpages {14}{16}}
+\headcommand {\sectionentry {3}{Distributionally Robust RL}{17}{Distributionally Robust RL}{0}}
+\headcommand {\slideentry {3}{0}{1}{17/17}{}{0}}
+\headcommand {\beamer@framepages {17}{17}}
+\headcommand {\slideentry {3}{0}{2}{18/18}{}{0}}
+\headcommand {\beamer@framepages {18}{18}}
+\headcommand {\slideentry {3}{0}{3}{19/19}{}{0}}
+\headcommand {\beamer@framepages {19}{19}}
+\headcommand {\beamer@sectionpages {17}{19}}
+\headcommand {\beamer@subsectionpages {17}{19}}
+\headcommand {\sectionentry {4}{Results}{20}{Results}{0}}
+\headcommand {\slideentry {4}{0}{1}{20/20}{}{0}}
+\headcommand {\beamer@framepages {20}{20}}
+\headcommand {\beamer@sectionpages {20}{20}}
+\headcommand {\beamer@subsectionpages {20}{20}}
+\headcommand {\sectionentry {5}{Conclusions}{21}{Conclusions}{0}}
+\headcommand {\slideentry {5}{0}{1}{21/21}{}{0}}
+\headcommand {\beamer@framepages {21}{21}}
+\headcommand {\slideentry {5}{0}{2}{22/22}{}{0}}
+\headcommand {\beamer@framepages {22}{22}}
+\headcommand {\slideentry {5}{0}{3}{23/23}{}{0}}
+\headcommand {\beamer@framepages {23}{23}}
+\headcommand {\gdef \insertmainframenumber {23}}
+\headcommand {\partentry {\translate {Appendix}}{1}}
+\headcommand {\beamer@partpages {1}{23}}
+\headcommand {\beamer@sectionpages {21}{23}}
+\headcommand {\beamer@subsectionpages {21}{23}}
+\headcommand {\beamer@appendixpages {24}}
+\headcommand {\beamer@sectionpages {24}{23}}
+\headcommand {\beamer@subsectionpages {24}{23}}
+\headcommand {\sectionentry {6}{Appendix}{24}{Appendix}{1}}
+\headcommand {\slideentry {6}{0}{1}{24/24}{}{1}}
+\headcommand {\beamer@framepages {24}{24}}
+\headcommand {\slideentry {6}{0}{2}{25/25}{}{1}}
+\headcommand {\beamer@framepages {25}{25}}
+\headcommand {\slideentry {6}{0}{3}{26/26}{}{1}}
+\headcommand {\beamer@framepages {26}{26}}
+\headcommand {\slideentry {6}{0}{4}{27/27}{}{1}}
+\headcommand {\beamer@framepages {27}{27}}
+\headcommand {\slideentry {6}{0}{5}{28/28}{}{1}}
+\headcommand {\beamer@framepages {28}{28}}
+\headcommand {\slideentry {6}{0}{6}{29/29}{}{1}}
+\headcommand {\beamer@framepages {29}{29}}
+\headcommand {\slideentry {6}{0}{7}{30/30}{}{1}}
+\headcommand {\beamer@framepages {30}{30}}
+\headcommand {\slideentry {6}{0}{8}{31/31}{}{1}}
+\headcommand {\beamer@framepages {31}{31}}
+\headcommand {\slideentry {6}{0}{9}{32/32}{}{1}}
+\headcommand {\beamer@framepages {32}{32}}
+\headcommand {\slideentry {6}{0}{10}{33/33}{}{1}}
+\headcommand {\beamer@framepages {33}{33}}
+\headcommand {\slideentry {6}{0}{11}{34/34}{}{1}}
+\headcommand {\beamer@framepages {34}{34}}
+\headcommand {\slideentry {6}{0}{12}{35/35}{}{1}}
+\headcommand {\beamer@framepages {35}{35}}
+\headcommand {\slideentry {6}{0}{13}{36/36}{}{1}}
+\headcommand {\beamer@framepages {36}{36}}
+\headcommand {\slideentry {6}{0}{14}{37/37}{}{1}}
+\headcommand {\beamer@framepages {37}{37}}
+\headcommand {\slideentry {6}{0}{15}{38/38}{}{1}}
+\headcommand {\beamer@framepages {38}{38}}
+\headcommand {\slideentry {6}{0}{16}{39/39}{}{1}}
+\headcommand {\beamer@framepages {39}{39}}
+\headcommand {\slideentry {6}{0}{17}{40/40}{}{1}}
+\headcommand {\beamer@framepages {40}{40}}
+\headcommand {\slideentry {6}{0}{18}{41/41}{}{1}}
+\headcommand {\beamer@framepages {41}{41}}
+\headcommand {\slideentry {6}{0}{19}{42/42}{}{1}}
+\headcommand {\beamer@framepages {42}{42}}
+\headcommand {\slideentry {6}{0}{20}{43/43}{}{1}}
+\headcommand {\beamer@framepages {43}{43}}
+\headcommand {\slideentry {6}{0}{21}{44/44}{}{1}}
+\headcommand {\beamer@framepages {44}{44}}
+\headcommand {\slideentry {6}{0}{22}{45/45}{}{1}}
+\headcommand {\beamer@framepages {45}{45}}
+\headcommand {\slideentry {6}{0}{23}{46/46}{}{1}}
+\headcommand {\beamer@framepages {46}{46}}
+\headcommand {\slideentry {6}{0}{24}{47/47}{}{1}}
+\headcommand {\beamer@framepages {47}{47}}
+\headcommand {\slideentry {6}{0}{25}{48/48}{}{1}}
+\headcommand {\beamer@framepages {48}{48}}
+\headcommand {\slideentry {6}{0}{26}{49/49}{}{1}}
+\headcommand {\beamer@framepages {49}{49}}
+\headcommand {\slideentry {6}{0}{27}{50/50}{}{1}}
+\headcommand {\beamer@framepages {50}{50}}
+\headcommand {\slideentry {6}{0}{28}{51/51}{}{1}}
+\headcommand {\beamer@framepages {51}{51}}
+\headcommand {\slideentry {6}{0}{29}{52/52}{}{1}}
+\headcommand {\beamer@framepages {52}{52}}
+\headcommand {\slideentry {6}{0}{30}{53/53}{}{1}}
+\headcommand {\beamer@framepages {53}{53}}
+\headcommand {\beamer@partpages {24}{53}}
+\headcommand {\beamer@subsectionpages {24}{53}}
+\headcommand {\beamer@sectionpages {24}{53}}
+\headcommand {\beamer@documentpages {53}}
+\headcommand {\gdef \inserttotalframenumber {30}}
+\headcommand {\gdef \inserttotalframenumber {23}}
+\headcommand {\gdef \appendixtotalframenumber {30}}
diff --git a/paper/defense/defense.pdf b/paper/defense/defense.pdf
new file mode 100644
index 0000000..6250078
Binary files /dev/null and b/paper/defense/defense.pdf differ
diff --git a/paper/defense/defense.snm b/paper/defense/defense.snm
new file mode 100644
index 0000000..e69de29
diff --git a/paper/defense/defense.tex b/paper/defense/defense.tex
new file mode 100644
index 0000000..9be36bf
--- /dev/null
+++ b/paper/defense/defense.tex
@@ -0,0 +1,932 @@
+% Final thesis defense (PHANTOM)
+% Build: cd paper/defense && pdflatex defense.tex && pdflatex defense.tex
+%
+% =========================================================================
+% NOTATION GUIDE (every variable used in this deck is defined once here)
+% =========================================================================
+%
+% PRICING & POLICY
+% p_t price action at time t [EUR per booking]
+% p_floor minimum viable price (marginal cost) [EUR per booking]
+% E[P] expected price under policy pi [EUR per booking]
+% pi(x_t) policy mapping context to price
+% x_t context vector (product, time, behavior signals)
+% R(p, q) revenue per session, equals p * q [EUR per session]
+%
+% COST OF INFORMATION
+% COI(pi) = E[P] - p_floor [EUR per transaction]
+% This is the average premium the platform extracts above marginal cost,
+% i.e. the financial value of "knowing the customer's interest".
+%
+% BEHAVIOR & SESSIONS
+% tau full session trajectory of (action, item, time) tuples
+% tau' partial trajectory observed at scoring time
+% T_hat(s'|s) empirical session transition kernel (a square table:
+% rows are current actions, columns are next actions,
+% each row sums to one)
+% T_H_bar human prototype kernel (reference for human cohort)
+% T_A_bar agent prototype kernel (reference for agent cohort)
+%
+% DETECTION SIGNAL
+% Delta_H = KL(T_hat' || T_H_bar) distance to human prototype
+% Delta_A = KL(T_hat' || T_A_bar) distance to agent prototype
+% g(tau') = Delta_H - Delta_A signed gap, zero is the boundary
+% f(tau') = sigmoid(g/T) in [0,1] agent-likelihood score
+% T temperature for the sigmoid (unitless)
+% threshold decision boundary at f = 0.5 (g = 0)
+%
+% DEMAND & CONTAMINATION
+% d(p|theta) individual demand response, theta is type
+% alpha contamination ratio, fraction of agent-mediated traffic
+% Q(p|alpha) aggregate demand under contamination alpha
+%
+% ROBUST CONTROL
+% P_hat_N empirical demand distribution from N samples
+% U_eps(P_hat_N) Wasserstein ambiguity ball of radius eps
+% lambda weight on COI-leakage penalty in the reward
+% eta_ux weight on UX penalty in the reward
+% c_info per-query info cost surrogate [EUR per query]
+% UX(tau, p) user-experience penalty in [0,1]
+%
+% =========================================================================
+
+\documentclass[aspectratio=169,11pt]{beamer}
+
+\usepackage[utf8]{inputenc}
+\usepackage[T1]{fontenc}
+\usepackage{lmodern}
+\usepackage{microtype}
+\usepackage{amsmath,amssymb}
+\usepackage{graphicx}
+\usepackage{xspace}
+\usepackage{booktabs}
+\usepackage{appendixnumberbeamer}
+\usepackage{hyperref}
+\usepackage{tikz}
+\usetikzlibrary{arrows.meta,calc,positioning,fit,shapes.geometric,shapes.misc,decorations.pathreplacing}
+
+\graphicspath{{../src/chapters/figures/results/generated/final/plots/}{../src/chapters/}}
+
+\usetheme[
+ progressbar=frametitle,
+]{moloch}
+\molochset{sectionpage=none,subsectionpage=none}
+\usefonttheme{professionalfonts}
+\setbeamertemplate{frame numbering}[fraction]
+
+% Dark palette: deep ink canvas, cream paper text, orange + teal accents
+\definecolor{PhantomPaper}{HTML}{F6F1E9}
+\definecolor{PhantomInk}{HTML}{0F1B2D}
+\definecolor{PhantomSlate}{HTML}{B5C2D2}
+\definecolor{PhantomCyan}{HTML}{E08A4C}
+\definecolor{PhantomIndigo}{HTML}{4FB8B2}
+\definecolor{PhantomPeach}{HTML}{EEC39C}
+\definecolor{PhantomMute}{HTML}{4A5566}
+
+\setbeamercolor{normal text}{fg=PhantomPaper,bg=PhantomInk}
+\setbeamercolor{background canvas}{bg=PhantomInk}
+\setbeamercolor{alerted text}{fg=PhantomCyan}
+\setbeamercolor{example text}{fg=PhantomIndigo}
+\setbeamercolor{palette primary}{fg=PhantomPaper,bg=PhantomInk}
+\setbeamercolor{frametitle}{fg=PhantomPaper,bg=PhantomInk}
+\setbeamercolor{progress bar}{fg=PhantomCyan,bg=PhantomMute!50}
+\setbeamercolor{title separator}{fg=PhantomCyan,bg=PhantomMute!50}
+\setbeamercolor{structure}{fg=PhantomCyan}
+\setbeamercolor{block title}{fg=PhantomPaper,bg=PhantomMute!70!PhantomInk}
+\setbeamercolor{block body}{fg=PhantomPaper,bg=PhantomMute!35!PhantomInk}
+\setbeamercolor{alertblock title}{fg=PhantomInk,bg=PhantomCyan}
+\setbeamercolor{alertblock body}{fg=PhantomPaper,bg=PhantomCyan!25!PhantomInk}
+\setbeamercolor{exampleblock title}{fg=PhantomInk,bg=PhantomIndigo}
+\setbeamercolor{exampleblock body}{fg=PhantomPaper,bg=PhantomIndigo!25!PhantomInk}
+
+\setbeamertemplate{navigation symbols}{}
+\setbeamertemplate{itemize item}{\small\raise0.3ex\hbox{$\bullet$}}
+\setbeamertemplate{itemize subitem}{\tiny\raise0.2ex\hbox{$\circ$}}
+
+\hypersetup{colorlinks=true,urlcolor=PhantomCyan,linkcolor=PhantomInk}
+
+\title{PHANTOM}
+\subtitle{Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms}
+\author{Daniel R\"osel}
+\institute{IE University, Madrid \\ Supervisor: Alberto Mart\'in Izquierdo}
+\date{\today}
+
+% Variable explanation block: \vardef{symbol}{description}
+\newcommand{\vardef}[2]{%
+ \par\noindent\makebox[1.55cm][l]{\textbf{\color{PhantomCyan}#1}}%
+ \parbox[t]{\dimexpr\linewidth-1.65cm}{\small #2}\par\smallskip%
+}
+
+\newcommand{\metriccard}[2]{%
+ \begin{tikzpicture}
+ \node[
+ draw=PhantomInk,
+ rounded corners=3pt,
+ fill=PhantomCyan!10,
+ minimum width=3.05cm,
+ minimum height=1.25cm,
+ align=center
+ ] {\Large\bfseries #1\\[-0.2em]{\scriptsize #2}};
+ \end{tikzpicture}%
+}
+
+% Compact inline icons (text-based, no nested tikzpictures).
+% Dynamic color: uses current text color so icons stay visible inside
+% blocks (dark text) and on the dark canvas (light text).
+\newcommand{\humanicon}{\textbf{\textsf{H}}\xspace}
+\newcommand{\roboticon}{\textcolor{PhantomCyan}{\textbf{\textsf{A}}}\xspace}
+\newcommand{\platformicon}{\textcolor{PhantomIndigo}{\textbf{\textsf{P}}}\xspace}
+
+% Big diagram icons (drawn directly inside an outer tikzpicture, no nesting)
+\newcommand{\bighumanicon}[1]{%
+ \fill[PhantomInk] (#1) ++(0,0.45) circle (0.18);
+ \draw[PhantomInk,line width=0.6pt,fill=PhantomSlate!30] (#1) ellipse (0.32 and 0.22);
+}
+\newcommand{\bigroboticon}[1]{%
+ \draw[PhantomInk,line width=0.6pt,rounded corners=2pt,fill=PhantomCyan!30] ($(#1)+(-0.42,-0.30)$) rectangle ($(#1)+(0.42,0.34)$);
+ \fill[PhantomPaper] ($(#1)+(-0.18,0.05)$) circle (0.09);
+ \fill[PhantomPaper] ($(#1)+(0.18,0.05)$) circle (0.09);
+ \fill[PhantomInk] ($(#1)+(-0.18,0.05)$) circle (0.035);
+ \fill[PhantomInk] ($(#1)+(0.18,0.05)$) circle (0.035);
+ \draw[PhantomInk,line width=0.6pt] ($(#1)+(0,0.34)$) -- ($(#1)+(0,0.50)$);
+ \fill[PhantomCyan] ($(#1)+(0,0.55)$) circle (0.05);
+}
+\newcommand{\bigplatformicon}[1]{%
+ \draw[PhantomInk,line width=0.6pt,rounded corners=2pt,fill=PhantomIndigo!25] ($(#1)+(-0.32,-0.32)$) rectangle ($(#1)+(0.32,0.32)$);
+ \node[font=\small\bfseries,text=PhantomInk] at (#1) {P};
+}
+
+\tikzset{every node/.append style={text=PhantomInk}}
+
+\begin{document}
+
+% =========================================================================
+% TITLE
+% =========================================================================
+\begin{frame}[plain]
+ \vfill
+ \centering
+ {\color{PhantomPaper}\Huge\bfseries PHANTOM\par}
+ \vspace{0.6em}
+ {\color{PhantomCyan}\rule{0.45\paperwidth}{0.06cm}\par}
+ \vspace{0.8em}
+ {\large\color{PhantomPaper}Pricing heuristics against non-human transaction orchestration\par}
+ \vfill
+ {\color{PhantomPaper}\normalsize Daniel R\"osel\par}
+ {\color{PhantomSlate}\small IE University \textbullet\ Supervisor: Alberto Mart\'in Izquierdo\par}
+ \vspace{1.2em}
+ {\footnotesize\color{PhantomCyan}\href{https://velocitatem.github.io/PHANTOM/}{\texttt{velocitatem.github.io/PHANTOM}}}
+ \vfill
+\end{frame}
+
+% =========================================================================
+% ROADMAP (no minutes)
+% =========================================================================
+\begin{frame}{Roadmap: one argument in six stages}
+ \centering
+ \begin{tikzpicture}[
+ font=\scriptsize\sffamily,
+ stage/.style={draw=PhantomInk,rounded corners=3pt,fill=PhantomCyan!10,minimum width=2.05cm,minimum height=0.95cm,align=center},
+ flow/.style={-{Stealth[length=2.0mm,width=1.8mm]},line width=1pt,PhantomSlate}
+ ]
+ \node[stage] (intro) {Two POVs};
+ \node[stage,right=0.30cm of intro] (platform) {Platform};
+ \node[stage,right=0.30cm of platform] (signal) {Signal};
+ \node[stage,right=0.30cm of signal] (drrl) {DR-RL};
+ \node[stage,right=0.30cm of drrl] (results) {Results};
+ \node[stage,right=0.30cm of results] (close) {Close};
+ \draw[flow,shorten <=2pt,shorten >=2pt] (intro.east) -- (platform.west);
+ \draw[flow,shorten <=2pt,shorten >=2pt] (platform.east) -- (signal.west);
+ \draw[flow,shorten <=2pt,shorten >=2pt] (signal.east) -- (drrl.west);
+ \draw[flow,shorten <=2pt,shorten >=2pt] (drrl.east) -- (results.west);
+ \draw[flow,shorten <=2pt,shorten >=2pt] (results.east) -- (close.west);
+ \end{tikzpicture}
+
+ \vspace{0.9em}
+ \begin{block}{Main research question}
+ How can dynamic pricing preserve margin integrity when transactions are
+ increasingly mediated by non-human agents?
+ \end{block}
+ \vspace{0.35em}
+ {\footnotesize Dynamic pricing has often been treated as a secondary
+ optimization layer; agent-mediated shopping turns it into a primary
+ margin-risk surface.}
+\end{frame}
+
+% =========================================================================
+% TWO POVs - INTRO
+% =========================================================================
+\begin{frame}{This work cycles through two points of view}
+ \centering
+ \begin{tikzpicture}[
+ font=\small\sffamily,
+ box/.style={draw=PhantomInk,rounded corners=6pt,minimum width=5.2cm,minimum height=3.4cm,align=center}
+ ]
+ \node[box,fill=PhantomCyan!12] (consumer) at (-3.4,0) {};
+ \node[box,fill=PhantomIndigo!12] (platform) at (3.4,0) {};
+ \bighumanicon{-3.4,0.95}
+ \bigplatformicon{3.4,0.95}
+ \node[font=\small,align=center,text=PhantomInk] at (-3.4,-0.45)
+ {\textbf{Consumer POV}\\[0.4em]
+ \footnotesize\textit{``how much does this item cost?''}\\[0.2em]
+ \footnotesize the question being asked};
+ \node[font=\small,align=center,text=PhantomInk] at (3.4,-0.45)
+ {\textbf{Platform POV}\\[0.4em]
+ \footnotesize\textit{``what does this session reveal?''}\\[0.2em]
+ \footnotesize the demand being inferred};
+ \draw[<->,thick,PhantomSlate] (consumer.east) -- node[above,font=\scriptsize,text=PhantomPaper]{price quote} node[below,font=\scriptsize,text=PhantomPaper]{behavior trail} (platform.west);
+ \end{tikzpicture}
+
+ \vspace{0.6em}
+ \begin{block}{Why two views}
+ The same transaction looks very different from each side. We will switch
+ between them to show where agent-mediation breaks the loop.
+ \end{block}
+\end{frame}
+
+% =========================================================================
+% POV 1 - DIRECT
+% =========================================================================
+\begin{frame}{POV 1: the consumer asks the platform directly}
+ \centering
+ \begin{tikzpicture}[
+ font=\small\sffamily,
+ actor/.style={draw=PhantomInk,rounded corners=5pt,minimum width=2.6cm,minimum height=1.4cm,align=center,fill=PhantomPaper},
+ msg/.style={draw=PhantomInk!40,rounded corners=2pt,fill=PhantomCyan!8,inner sep=4pt,font=\scriptsize}
+ ]
+ \node[actor,fill=PhantomCyan!14] (h) at (-4.5,0) {\\[0.6em]consumer};
+ \node[actor,fill=PhantomIndigo!14] (p) at (4.5,0) {\\[0.6em]platform};
+ \bighumanicon{-4.5,0.25}
+ \bigplatformicon{4.5,0.25}
+
+ \draw[-{Stealth[length=2.5mm]},thick,PhantomPaper] ([yshift=0.45cm]h.east) -- ([yshift=0.45cm]p.west)
+ node[midway,above,msg]{``how much does item $i$ cost?''};
+ \draw[-{Stealth[length=2.5mm]},thick,PhantomPaper] ([yshift=-0.45cm]p.west) -- ([yshift=-0.45cm]h.east)
+ node[midway,below,msg]{``it costs $p_t$''};
+ \end{tikzpicture}
+
+ \vspace{0.7em}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.5\textwidth}
+ \footnotesize
+ \textbf{What you see:} a website opens, you read a price, you decide.\\[0.3em]
+ \textbf{What the platform sees:} clicks, hovers, dwell time --- a clean
+ behavioral fingerprint of one human session.
+ \column{0.45\textwidth}
+ \begin{block}{Variables}
+ \vardef{$i$}{the item being shopped (e.g. a hotel night)}
+ \vardef{$p_t$}{posted price at time $t$, in EUR per booking}
+ \end{block}
+ \end{columns}
+\end{frame}
+
+% =========================================================================
+% POV 2 - VIA AGENT
+% =========================================================================
+\begin{frame}{POV 2: the consumer asks an AI agent}
+ \centering
+ \begin{tikzpicture}[
+ font=\small\sffamily,
+ actor/.style={draw=PhantomInk,rounded corners=5pt,minimum width=2.3cm,minimum height=1.3cm,align=center,fill=PhantomPaper},
+ msg/.style={font=\tiny,text=PhantomSlate}
+ ]
+ \node[actor,fill=PhantomCyan!14] (h) at (-5.6,0) {};
+ \node[actor,fill=PhantomCyan!14] (a) at (0,0) {};
+ \node[actor,fill=PhantomIndigo!14] (p) at (5.6,0) {};
+ \bighumanicon{-5.6,0}
+ \bigroboticon{0,0}
+ \bigplatformicon{5.6,0}
+
+ \draw[-{Stealth[length=2.5mm]},thick,PhantomPaper] ([yshift=0.35cm]h.east) -- ([yshift=0.35cm]a.west)
+ node[midway,above,msg]{``find prices for $i$''};
+ \draw[-{Stealth[length=2.5mm]},thick,PhantomPaper] ([yshift=0.35cm]a.east) -- ([yshift=0.35cm]p.west)
+ node[midway,above,msg]{repeated queries};
+ \draw[-{Stealth[length=2.5mm]},thick,PhantomCyan] ([yshift=-0.35cm]p.west) -- ([yshift=-0.35cm]a.east)
+ node[midway,below,msg,text=PhantomCyan]{many quotes};
+ \draw[-{Stealth[length=2.5mm]},thick,PhantomCyan] ([yshift=-0.35cm]a.west) -- ([yshift=-0.35cm]h.east)
+ node[midway,below,msg,text=PhantomCyan]{best price found};
+ \end{tikzpicture}
+
+ \vspace{0.5em}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.55\textwidth}
+ \footnotesize
+ \textbf{Same intent, different visible behavior.} The platform sees a
+ machine-paced session that looks nothing like the human who actually
+ wants to buy.
+ \column{0.40\textwidth}
+ \begin{alertblock}{Information asymmetry flips}
+ The agent samples many quotes before committing; the platform only
+ sees the recon, not the buyer.
+ \end{alertblock}
+ \end{columns}
+\end{frame}
+
+% =========================================================================
+% DEMAND FLOW - WHAT THE PLATFORM ACTUALLY OBSERVES
+% =========================================================================
+\begin{frame}{Two flows, one demand signal --- and only one is reliable}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily,
+ flow/.style={-{Stealth[length=2.2mm]},thick,PhantomSlate},
+ panel/.style={draw=PhantomInk,rounded corners=4pt,minimum width=5.5cm,minimum height=3.0cm,align=center,fill=PhantomPaper}]
+
+ \node[panel,fill=PhantomCyan!8] (left) at (-3.4,0) {};
+ \node[anchor=north,font=\footnotesize\bfseries] at (left.north) {\vphantom{p}direct human session};
+ \coordinate (h1) at (-5.0,0.0);
+ \bighumanicon{h1}
+ \node[draw=PhantomInk,rounded corners=2pt,minimum width=1.4cm,minimum height=0.55cm,fill=PhantomPaper,font=\tiny] (pt1) at (-2.0,0.6) {one buyer};
+ \node[draw=PhantomInk,rounded corners=2pt,minimum width=1.4cm,minimum height=0.55cm,fill=PhantomCyan!20,font=\tiny] (q1) at (-2.0,-0.6) {true demand};
+ \draw[flow] ($(h1)+(0.3,0)$) -- (pt1.west);
+ \draw[flow] ($(h1)+(0.3,0)$) -- (q1.west);
+
+ \node[panel,fill=PhantomIndigo!8] (right) at (3.4,0) {};
+ \node[anchor=north,font=\footnotesize\bfseries] at (right.north) {\vphantom{p}agent-mediated session};
+ \coordinate (h2) at (1.4,0.0);
+ \coordinate (a2) at (3.4,0.0);
+ \bighumanicon{h2}
+ \bigroboticon{a2}
+ \node[draw=PhantomInk,rounded corners=2pt,minimum width=1.4cm,minimum height=0.55cm,fill=PhantomPaper,font=\tiny] (pt2) at (5.4,0.6) {many quotes};
+ \node[draw=PhantomInk,rounded corners=2pt,minimum width=1.4cm,minimum height=0.55cm,fill=PhantomMute!30,font=\tiny] (q2) at (5.4,-0.6) {buyer hidden};
+ \draw[flow] ($(h2)+(0.3,0)$) -- ($(a2)+(-0.42,0)$);
+ \draw[flow] ($(a2)+(0.42,0)$) -- (pt2.west);
+ \draw[flow] ($(a2)+(0.42,0)$) -- (q2.west);
+ \end{tikzpicture}
+
+ \vspace{0.5em}
+ {\footnotesize\textbf{Takeaway.} On the right, the platform sees recon, not
+ intent. Pricing trained on the visible signal will misread real demand.}
+\end{frame}
+
+% =========================================================================
+% POLICY DEFINITION (variables explained)
+% =========================================================================
+\begin{frame}{Policy first: one rule maps context into a price}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.50\textwidth}
+ \begin{block}{Definition}
+ \[
+ p_t = \pi(x_t)
+ \]
+ \end{block}
+ \vardef{$p_t$}{price quoted at time $t$ \;[EUR per booking]}
+ \vardef{$\pi$}{the pricing policy --- a function the platform learns}
+ \vardef{$x_t$}{context: product, time-of-day, and behavior summary of the session}
+
+ \column{0.45\textwidth}
+ \centering
+ \begin{tikzpicture}[
+ font=\scriptsize\sffamily,
+ box/.style={draw=PhantomInk,rounded corners=4pt,minimum width=3.35cm,minimum height=0.85cm,align=center},
+ flow/.style={-{Stealth[length=2.0mm]},thick,PhantomSlate}
+ ]
+ \node[box,fill=PhantomPaper] (ctx) at (0,1.35) {context $x_t$};
+ \node[box,fill=PhantomIndigo!12] (pol) at (0,0.15) {policy $\pi$};
+ \node[box,fill=PhantomCyan!15] (act) at (0,-1.05) {price $p_t$};
+ \draw[flow] (ctx) -- (pol);
+ \draw[flow] (pol) -- (act);
+ \node[font=\tiny\itshape,text=PhantomSlate] at (0,-1.75) {bandits first; later extended to DR-RL};
+ \end{tikzpicture}
+ \end{columns}
+\end{frame}
+
+% =========================================================================
+% COI METRIC (matches reference image style)
+% =========================================================================
+\begin{frame}{Cost of Information (COI) --- what the platform earns from \alert{knowing you}}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.50\textwidth}
+ \begin{block}{Definition}
+ \vspace{0.2em}
+ \centering
+ \[
+ \mathrm{COI}(\pi) = \mathbb{E}[P] - p_{\mathrm{floor}}
+ \]
+ \end{block}
+ \vardef{$\mathbb{E}[P]$}{expected price the policy actually charges \;[EUR per booking]}
+ \vardef{$p_{\mathrm{floor}}$}{minimum viable price (marginal cost / break-even floor)}
+ \vardef{COI}{average premium the platform extracts above marginal cost \;[EUR per transaction]}
+
+ \column{0.48\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily,x=0.95cm,y=0.85cm]
+ \draw[->,thick,PhantomPaper] (-0.2,0) -- (5.6,0) node[right,text=PhantomPaper]{price $p$};
+ \draw[->,thick,PhantomPaper] (0,-0.1) -- (0,3.0) node[above,text=PhantomPaper]{density};
+ \draw[very thick,PhantomPaper,domain=0.2:5.2,smooth,samples=80]
+ plot (\x, {2.5*exp(-((\x-2.6)^2)/1.0)});
+ \draw[dashed,PhantomSlate] (1.2,0) -- (1.2,2.0) node[above,font=\tiny,text=PhantomPaper]{$p_{\mathrm{floor}}$};
+ \draw[dashed,PhantomCyan,thick] (2.6,0) -- (2.6,2.5) node[above,font=\tiny,text=PhantomCyan]{$\mathbb{E}[P]$};
+ \draw[decorate,decoration={brace,amplitude=4pt},PhantomCyan,thick] (1.2,2.7) -- (2.6,2.7) node[midway,above,font=\scriptsize,text=PhantomCyan]{COI};
+ \end{tikzpicture}
+ \end{columns}
+
+ \vspace{0.3em}
+ {\footnotesize\textit{The ``cost'' is from the consumer's POV: it is what they pay because the platform can read their interest. Revenue at risk equals COI $\times$ volume.}}
+\end{frame}
+
+% =========================================================================
+% WHY AGENTS ERODE COI - SIMPLIFIED ORDER STATISTIC
+% =========================================================================
+\begin{frame}{Why agents erode COI: the realizable price drops to the minimum}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.46\textwidth}
+ \footnotesize
+ A single buyer pays the price they were quoted.\\[0.4em]
+ An agent samples $N$ independent quotes and the buyer pays
+ \(\;p^{(1)} = \min(p_1,\dots,p_N)\).\\[0.4em]
+ \textbf{Result:} as $N \to \infty$, $\mathrm{COI} \to 0$. More recon
+ pushes realizable prices toward the floor.
+
+ \vspace{0.5em}
+ \begin{alertblock}{One-line claim}
+ Untreated agentic recon behaves like an information leak that
+ compresses sustainable margins.
+ \end{alertblock}
+
+ \column{0.50\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily,x=0.65cm,y=0.85cm]
+ \draw[->,thick,PhantomPaper] (0,0) -- (6.5,0) node[right,text=PhantomPaper]{queries $N$};
+ \draw[->,thick,PhantomPaper] (0,0) -- (0,2.6) node[above,text=PhantomPaper]{COI [EUR]};
+ \draw[very thick,PhantomCyan,domain=0.2:6.0,smooth,samples=60]
+ plot (\x, {2.2*exp(-0.55*\x)+0.12});
+ \draw[dashed,PhantomSlate] (0,0.12) -- (6.0,0.12);
+ \node[anchor=west,font=\tiny,text=PhantomSlate] at (3.4,0.32) {price-floor proximity};
+ \node[anchor=west,font=\tiny,text=PhantomPaper] at (0.4,2.2) {single human};
+ \node[anchor=west,font=\tiny,text=PhantomCyan] at (3.4,1.0) {agent-amplified};
+ \end{tikzpicture}
+ \end{columns}
+\end{frame}
+
+% =========================================================================
+% RESEARCH QUESTIONS - SQ1 SQ2 SQ3
+% =========================================================================
+\begin{frame}{The thesis answers one chain: \alert{mechanism \(\to\) signal \(\to\) control}}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.32\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily]
+ \draw[rounded corners=4pt,draw=PhantomInk,fill=PhantomPaper] (-1.55,-1.1) rectangle (1.55,1.2);
+ \fill[PhantomCyan] (-0.75,0.35) circle (0.14);
+ \fill[PhantomCyan] (-0.45,0.70) circle (0.14);
+ \fill[PhantomCyan] (-0.15,0.45) circle (0.14);
+ \fill[PhantomIndigo] (0.35,-0.20) circle (0.14);
+ \fill[PhantomIndigo] (0.65,-0.45) circle (0.14);
+ \fill[PhantomIndigo] (0.95,-0.15) circle (0.14);
+ \draw[dashed,PhantomInk!60] (0.12,-0.92) -- (0.12,1.0);
+ \node[text=PhantomMute,font=\tiny] at (0,-0.97) {behavior separability};
+ \end{tikzpicture}
+ {\footnotesize\textbf{SQ1}}\\[-0.15em]
+ {\scriptsize Can we distinguish \humanicon and \roboticon sessions from interactions alone?}
+
+ \column{0.32\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily]
+ \draw[rounded corners=4pt,draw=PhantomInk,fill=PhantomPaper] (-1.55,-1.1) rectangle (1.55,1.2);
+ \draw[->,thick,PhantomMute] (-1.15,-0.75) -- (1.2,-0.75) node[right,font=\tiny,text=PhantomMute,xshift=-3pt,yshift=4pt]{$\alpha$};
+ \draw[->,thick,PhantomMute] (-1.15,-0.75) -- (-1.15,0.85) node[above,font=\tiny,text=PhantomMute,yshift=-4pt,xshift=8pt]{COI};
+ \draw[very thick,PhantomCyan,domain=-1.0:1.05,smooth,samples=40] plot (\x, {0.65*exp(-1.2*(\x+1.0))-0.05});
+ \draw[dashed,PhantomMute] (-1.15,-0.55) -- (1.05,-0.55);
+ \node[text=PhantomMute,font=\tiny,xshift=2pt] at (-0.4,-0.45) {floor};
+ \node[text=PhantomMute,font=\tiny] at (0,-0.97) {COI erodes as $\alpha\uparrow$};
+ \end{tikzpicture}
+ {\footnotesize\textbf{SQ2}}\\[-0.15em]
+ {\scriptsize How strong is price and revenue erosion under agentic contamination?}
+
+ \column{0.32\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily]
+ \draw[rounded corners=4pt,draw=PhantomInk,fill=PhantomPaper] (-1.55,-1.1) rectangle (1.55,1.2);
+ \draw[->,thick,PhantomMute] (-1.15,-0.75) -- (1.2,-0.75) node[right,font=\tiny,text=PhantomMute,xshift=-3pt,yshift=4pt]{UX};
+ \draw[->,thick,PhantomMute] (-1.15,-0.75) -- (-1.15,0.85) node[above,font=\tiny,text=PhantomMute,yshift=-4pt,xshift=10pt]{margin};
+ \fill[PhantomSlate] (-0.55,-0.40) circle (2.6pt);
+ \node[text=PhantomMute,font=\tiny] at (-0.55,-0.13) {baseline};
+ \fill[PhantomCyan] (0.55,0.45) circle (2.8pt);
+ \node[text=PhantomCyan,font=\tiny] at (0.55,0.72) {robust};
+ \draw[->,thick,PhantomCyan,dashed] (-0.40,-0.30) -- (0.40,0.35);
+ \node[text=PhantomMute,font=\tiny] at (0,-0.97) {robust dominates baseline};
+ \end{tikzpicture}
+ {\footnotesize\textbf{SQ3}}\\[-0.15em]
+ {\scriptsize Can policy design recover margin while keeping UX stable?}
+ \end{columns}
+\end{frame}
+
+\section{Platform Development}
+
+% =========================================================================
+% PLATFORM (Stage 1)
+% =========================================================================
+\begin{frame}{Stage 1: a dual-loop platform pairs every quote with its behavior}
+ \centering
+ \begin{tikzpicture}[
+ font=\scriptsize\sffamily,
+ box/.style={draw=PhantomInk,rounded corners=3pt,minimum width=2.5cm,minimum height=0.9cm,align=center,fill=PhantomPaper},
+ arr/.style={-{Stealth[length=2.2mm]},thick,PhantomSlate}
+ ]
+ \node[box,fill=PhantomCyan!14] (actors) at (0,1.45) {users \humanicon\\agents \roboticon};
+ \node[box] (web) at (2.9,1.45) {web\\storefront};
+ \node[box] (provider) at (5.8,1.45) {pricing\\service};
+ \node[box] (redis) at (8.7,1.45) {serve\\cache};
+ \node[box,fill=PhantomIndigo!12,minimum width=3.1cm] (kafka) at (4.35,-0.15) {event stream\\behavior + quote logs};
+ \node[box,fill=PhantomCyan!10,minimum width=2.8cm] (airflow) at (8.0,-0.15) {offline trainer\\batch updates};
+
+ \draw[arr] (actors) -- (web);
+ \draw[arr] (web) -- (provider);
+ \draw[arr] (provider) -- (redis);
+ \draw[arr] (web.south) -- (kafka.north west);
+ \draw[arr] (provider.south) -- (kafka.north east);
+ \draw[arr] (kafka) -- (airflow);
+ \draw[arr] (airflow.north) -| (redis.south);
+ \draw[arr] (redis.west) to[bend left=35] (provider.east);
+
+ \node[font=\tiny\itshape,text=PhantomSlate] at (2.2,-1.0) {Kappa: streaming telemetry};
+ \node[font=\tiny\itshape,text=PhantomSlate] at (8.1,-1.0) {Lambda: offline learning + refresh};
+ \end{tikzpicture}
+
+ \vspace{0.4em}
+ \begin{itemize}
+ \item Every quote has a matching behavioral context in the log stream.
+ \item The same architecture supports reproducible stress tests before any live deployment.
+ \end{itemize}
+\end{frame}
+
+% =========================================================================
+% DATASET CARD
+% =========================================================================
+\begin{frame}{Dataset card: compact, labeled, experiment-ready}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.60\textwidth}
+ \centering
+ \begin{tikzpicture}[
+ font=\scriptsize\sffamily,
+ chip/.style={draw=PhantomInk!40,rounded corners=2pt,inner sep=2.7pt,fill=PhantomPaper}
+ ]
+ \node[draw=PhantomInk,rounded corners=5pt,fill=PhantomPaper,minimum width=6.85cm,minimum height=4.45cm] at (0,0) {};
+ \node[anchor=west,font=\footnotesize\bfseries,text=PhantomInk] at (-3.2,1.72) {WhoClickedIt dataset card};
+ \node[anchor=west,draw=PhantomInk!35,rounded corners=2pt,fill=PhantomCyan!10,inner xsep=4pt,inner ysep=3pt,text width=6.15cm,align=left,font=\tiny\ttfamily,text=PhantomInk] at (-3.2,1.22)
+ {huggingface.co/datasets/velocitatem/whoclickedit};
+ \node[anchor=west,chip,fill=PhantomIndigo!12] (humanrows) at (-3.2,0.52) {\textbf{human rows} 798};
+ \node[anchor=west,chip,fill=PhantomIndigo!12] at ([xshift=0.16cm]humanrows.east) {\textbf{agent rows} 3076};
+ \node[anchor=west,text width=6.0cm,align=left,font=\scriptsize,text=PhantomInk] at (-3.2,-0.33)
+ {Flat schema and explicit actor labels simplify session-aware train/test splits.};
+ \node[anchor=west,font=\tiny\itshape,text=PhantomSlate] at (-3.2,-1.01)
+ {Kafka provenance is retained for reproducibility.};
+ \end{tikzpicture}
+
+ \column{0.38\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily,
+ stat/.style={draw=PhantomInk,rounded corners=5pt,minimum width=4.95cm,minimum height=1.33cm,align=center}]
+ \node[stat,fill=PhantomCyan!12] at (0,1.95)
+ {\Large\bfseries 29 interviews\\[-0.1em]\footnotesize labeled trajectories};
+ \node[stat,fill=PhantomCyan!18] at (0,0.25)
+ {\Large\bfseries 45\% / 55\%\\[-0.1em]\footnotesize human / agent split};
+ \node[stat,fill=PhantomIndigo!12] at (0,-1.45)
+ {\Large\bfseries 2 streams\\[-0.1em]\footnotesize interaction + price logs};
+ \end{tikzpicture}
+ \end{columns}
+\end{frame}
+
+% =========================================================================
+% EXPERIMENTAL DESIGN
+% =========================================================================
+\begin{frame}{Experimental design controls goals, not instructions}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.58\textwidth}
+ \centering
+ \begin{tikzpicture}[
+ font=\scriptsize\sffamily,
+ box/.style={draw=PhantomInk,rounded corners=3pt,minimum width=3.65cm,minimum height=0.95cm,align=center,fill=PhantomPaper},
+ arr/.style={-{Stealth[length=2.2mm]},thick,PhantomSlate}
+ ]
+ \node[box,fill=PhantomCyan!14] (tasks) at (0,1.8) {JTBD task pool\\hotel + airline modes};
+ \node[box] (assign) at (0,0.55) {random assignment\\mode + task + actor id};
+ \node[box,fill=PhantomIndigo!12] (run) at (0,-0.7) {execution\\human or browser-use agent};
+ \node[box] (logs) at (0,-1.95) {session logs\\events + quotes};
+ \draw[arr] (tasks) -- (assign);
+ \draw[arr] (assign) -- (run);
+ \draw[arr] (run) -- (logs);
+ \end{tikzpicture}
+
+ \column{0.40\textwidth}
+ \begin{itemize}\setlength{\itemsep}{0.55em}
+ \item Agents run with \textbf{browser-use} and a model-swappable LLM router (default \texttt{gpt-5-mini}).
+ \item Tasks are defined by outcomes, not scripted clicks, to preserve behavioral variety.
+ \item Current release is stronger on hotel flows than airline flows.
+ \end{itemize}
+ \end{columns}
+\end{frame}
+
+\section{Distinguishability Construction}
+
+% =========================================================================
+% KERNEL EXPLAINER (NEW dedicated slide)
+% =========================================================================
+\begin{frame}{Stage 2: what is a \alert{kernel}?}
+ \begin{block}{Plain definition}
+ A \textbf{kernel} is a small square table $T$ where $T[a,b]$ is the
+ probability that action $b$ follows action $a$ inside one session. Every
+ row sums to one.
+ \end{block}
+
+ \vspace{0.3em}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.45\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily]
+ \node[draw=PhantomInk,rounded corners=3pt,fill=PhantomCyan!12,minimum width=2.6cm,minimum height=0.7cm] (a) at (-1.6,0.7) {view item};
+ \node[draw=PhantomInk,rounded corners=3pt,fill=PhantomPaper,minimum width=2.6cm,minimum height=0.7cm] (b) at (1.6,0.7) {hover};
+ \node[draw=PhantomInk,rounded corners=3pt,fill=PhantomIndigo!12,minimum width=2.6cm,minimum height=0.7cm] (c) at (0,-0.9) {add to cart};
+ \draw[-{Stealth[length=2mm]},thick,PhantomSlate] (a) -- node[above,font=\tiny]{0.64} (b);
+ \draw[-{Stealth[length=2mm]},thick,PhantomSlate] (b) -- node[right,font=\tiny]{0.31} (c);
+ \draw[-{Stealth[length=2mm]},thick,PhantomSlate] (a) -- node[left,font=\tiny]{0.36} (c);
+ \end{tikzpicture}\\[0.3em]
+ {\tiny example session graph}
+
+ \column{0.50\textwidth}
+ \centering
+ \begin{tabular}{@{}l|ccc@{}}
+ \toprule
+ from \textbackslash{} to & view & hover & cart \\
+ \midrule
+ view & 0.00 & 0.64 & 0.36 \\
+ hover & 0.69 & 0.00 & 0.31 \\
+ cart & 0.00 & 0.00 & 1.00 \\
+ \bottomrule
+ \end{tabular}\\[0.4em]
+ {\footnotesize\textit{This is the kernel $T$. Each row is a probability distribution.}}
+ \end{columns}
+
+ \vspace{0.3em}
+ \vardef{$T[a,b]$}{probability that the next action is $b$ given the current action is $a$}
+\end{frame}
+
+% =========================================================================
+% HUMAN VS AGENT KERNELS - VISUAL COMPARISON
+% =========================================================================
+\begin{frame}{Humans and agents click in \alert{different patterns}}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.48\textwidth}
+ \centering
+ \textbf{Human kernel $\bar T_H$}\par
+ {\scriptsize view $\to$ hover $\to$ cart, with detours}\par\vspace{0.2em}
+ \includegraphics[width=\linewidth,height=0.40\textheight,keepaspectratio]{mdp_human.pdf}
+ \column{0.48\textwidth}
+ \centering
+ \textbf{Agent kernel $\bar T_A$}\par
+ {\scriptsize view $\to$ view $\to$ view, almost no cart}\par\vspace{0.2em}
+ \includegraphics[width=\linewidth,height=0.40\textheight,keepaspectratio]{mdp_agent.pdf}
+ \end{columns}
+
+ \vspace{0.4em}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.32\textwidth}\centering\metriccard{$-3.35$}{mean gap (human)}
+ \column{0.32\textwidth}\centering\metriccard{$+1.65$}{mean gap (agent)}
+ \column{0.32\textwidth}\centering\metriccard{$p<0.001$}{Mann-Whitney rank}
+ \end{columns}
+ \vspace{0.2em}
+ {\footnotesize\textit{Two cohorts, two clearly separable click structures --- this is the foundation of the detection signal.}}
+\end{frame}
+
+% =========================================================================
+% SIGMOID SCORE - SIMPLIFIED
+% =========================================================================
+\begin{frame}{From two divergences to one \alert{sigmoid score}}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.46\textwidth}
+ \begin{block}{Step 1 --- distance to each prototype}
+ \(\Delta_H = \mathrm{KL}(\hat T' \,\|\, \bar T_H)\)\\[0.2em]
+ \(\Delta_A = \mathrm{KL}(\hat T' \,\|\, \bar T_A)\)
+ \end{block}
+ \begin{block}{Step 2 --- signed gap}
+ \(g(\tau') = \Delta_H - \Delta_A\)
+ \end{block}
+ \begin{alertblock}{Step 3 --- \textbf{sigmoid} squash}
+ \(f(\tau') = \sigma\!\left(\dfrac{g(\tau')}{T}\right) \in [0,1]\)
+ \end{alertblock}
+ \vardef{$\sigma$}{the standard logistic sigmoid}
+ \vardef{$T$}{temperature, controls how sharply the score moves away from $0.5$}
+ \vardef{threshold}{$f = 0.5$ corresponds to $g = 0$ (neither side)}
+
+ \column{0.50\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily,x=0.7cm,y=2.0cm]
+ \draw[->,thick,PhantomPaper] (-4.2,0) -- (4.4,0) node[right,text=PhantomPaper]{$g(\tau')$};
+ \draw[->,thick,PhantomPaper] (0,-0.05) -- (0,1.15) node[above,text=PhantomPaper]{$f(\tau')$};
+ \draw[dashed,PhantomMute] (-4.0,1.0) -- (4.0,1.0);
+ \draw[dashed,PhantomCyan,thick] (-4.0,0.5) -- (4.0,0.5) node[right,font=\tiny,text=PhantomCyan]{threshold $0.5$};
+ \draw[very thick,PhantomCyan,domain=-4.0:4.0,smooth,samples=120]
+ plot (\x, {1/(1+exp(-1.4*\x))});
+ \node[anchor=north,font=\tiny,text=PhantomPaper] at (-3.0,-0.04) {human-like \humanicon};
+ \node[anchor=north,font=\tiny,text=PhantomPaper] at (3.0,-0.04) {agent-like \roboticon};
+ \fill[PhantomCyan] (0,0.5) circle (1.6pt);
+ \node[anchor=west,font=\tiny,text=PhantomPaper] at (0.1,0.55) {$g=0$};
+ \end{tikzpicture}\\[0.2em]
+ {\tiny\color{PhantomSlate} logistic curve --- saturates at $0$ and $1$, threshold at $0.5$}
+ \end{columns}
+\end{frame}
+
+\section{Distributionally Robust RL}
+
+% =========================================================================
+% DR-RL CORE IDEA - SIMPLIFIED
+% =========================================================================
+\begin{frame}{Stage 3: DR-RL trains against \alert{many plausible worlds}}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.50\textwidth}
+ \footnotesize
+ Standard RL trains against one demand model and overfits to it.
+ \textbf{DR-RL} optimises the worst case across a small ball of
+ plausible demand laws, so the policy still works when contamination
+ shifts.
+
+ \vspace{0.6em}
+ \begin{block}{Robust objective}
+ \(\displaystyle \pi^\star = \arg\max_\pi \min_{Q \in U_\epsilon} \mathbb{E}_Q[\,r\,]\)
+ \end{block}
+ \vardef{$Q$}{a candidate demand distribution inside the ball}
+ \vardef{$U_\epsilon$}{Wasserstein ball of radius $\epsilon$ around the empirical $\hat P_N$}
+ \vardef{$r$}{per-step reward (defined next slide)}
+
+ \column{0.46\textwidth}
+ \centering
+ \begin{tikzpicture}[font=\scriptsize\sffamily]
+ \draw[thick,PhantomPaper,fill=PhantomCyan!25!PhantomInk] (0,0) circle (2.0);
+ \draw[dashed,PhantomCyan,thick] (0,0) circle (1.4);
+ \fill[PhantomPaper] (0,0) circle (2.5pt) node[below,font=\tiny,text=PhantomPaper]{$\hat P_N$};
+ \fill[PhantomCyan] (0.9,0.5) circle (2pt);
+ \fill[PhantomCyan] (-0.7,0.7) circle (2pt);
+ \fill[PhantomCyan] (-1.0,-0.4) circle (2pt);
+ \fill[PhantomCyan] (0.6,-0.9) circle (2pt);
+ \node[font=\tiny,text=PhantomCyan] at (0,1.7) {plausible $Q$};
+ \node[font=\tiny,text=PhantomPaper] at (0,-2.3) {ambiguity ball $U_\epsilon$};
+ \end{tikzpicture}
+ \end{columns}
+
+ \vspace{0.3em}
+ {\footnotesize\textit{Implementation note: in code we solve a local robust loop on the contamination parameter $\alpha$, not the full continuous Wasserstein adversary.}}
+\end{frame}
+
+% =========================================================================
+% REWARD - THREE TERMS, EACH EXPLAINED
+% =========================================================================
+\begin{frame}{Reward: revenue, minus leakage, minus UX cost}
+ \[
+ r_t \;=\; \underbrace{R(p_t,\hat Q_t)}_{\text{revenue}}
+ \;-\; \underbrace{\lambda \, f(\tau'_t)\, c_{\text{info}}}_{\text{leakage penalty}}
+ \;-\; \underbrace{\eta_{\text{ux}}\, \mathrm{UX}(\tau'_t,p_t)}_{\text{UX penalty}}
+ \]
+
+ \vspace{0.6em}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.32\textwidth}
+ \begin{block}{Revenue}
+ \(R(p_t,\hat Q_t) = p_t \cdot \hat Q_t\)\\[0.2em]
+ {\footnotesize units: EUR per session}
+ \end{block}
+ \column{0.32\textwidth}
+ \begin{alertblock}{Leakage}
+ scales with $f(\tau'_t)$ \roboticon\\
+ $\lambda$: weight\\
+ $c_{\text{info}}$: per-query cost
+ \end{alertblock}
+ \column{0.32\textwidth}
+ \begin{exampleblock}{UX}
+ $\mathrm{UX}\in[0,1]$\\
+ $\eta_{\text{ux}}$: weight\\
+ penalises unstable pricing
+ \end{exampleblock}
+ \end{columns}
+
+ \vspace{0.4em}
+ {\footnotesize \textbf{Reading the formula:}
+ if a session looks agent-like ($f \uparrow$), the leakage penalty grows
+ and the policy backs off; for clean human sessions only the revenue and
+ UX terms are active.}
+\end{frame}
+
+% =========================================================================
+% COMPUTE
+% =========================================================================
+\begin{frame}{Wide sweeps are feasible only with \alert{aggressive optimization}}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.47\textwidth}
+ \centering
+ {\Large\(4\times4\times3\times2\times2=\mathbf{192}\) configs}\\[0.25em]
+ {\scriptsize algorithms $\times$ contamination $\times$ robustness $\times$ COI penalty $\times$ action grid}
+
+ \vspace{0.5em}
+ \metriccard{160 PFLOPS}{peak aggregate TPU budget}\\[0.45em]
+ \metriccard{$\sim$180 days}{net compute logged}
+
+ \column{0.51\textwidth}
+ \begin{block}{Hot-path rewrite impact}
+ \centering
+ \begin{tabular}{@{}lcc@{}}
+ \toprule
+ Mode & Before & After \\
+ \midrule
+ Baseline step/s & 26.0 & 220.0 \\
+ Robust step/s & 7.2 & 136.0 \\
+ \bottomrule
+ \end{tabular}
+ \end{block}
+ \vspace{0.1em}
+ {\footnotesize
+ \begin{itemize}
+ \item pandas lookups replaced with array/JAX-style loops.
+ \item $8.5\times$ and $19\times$ throughput gains made wide sweeps practical.
+ \end{itemize}}
+ \end{columns}
+\end{frame}
+
+\section{Results}
+
+% =========================================================================
+% RESULTS - BIGGER FONTS, EXPLICIT CONCLUSION
+% =========================================================================
+\begin{frame}{Defended policies recover COI as contamination rises}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.62\textwidth}
+ \centering
+ \includegraphics[width=\linewidth,height=0.66\textheight,keepaspectratio]{final_focus_coi_by_alpha.pdf}\\[0.2em]
+ {\footnotesize x: contamination $\alpha$ (fraction of agent traffic) \;|\; y: COI [EUR per transaction]}
+
+ \column{0.34\textwidth}
+ \metriccard{$-90{,}140$}{baseline COI slope (EUR per unit $\alpha$)}\\[0.35em]
+ \metriccard{$\sim 3\%$}{short-run revenue cost of defense}\\[0.35em]
+ \metriccard{regime-dependent}{COI gains strongest at higher $\alpha$}
+
+ \vspace{0.5em}
+ \begin{alertblock}{Conclusion}
+ \footnotesize Without defense, COI collapses with $\alpha$.
+ Robust policies hold a measurable margin floor at the cost of
+ a small, bounded revenue trade.
+ \end{alertblock}
+ \end{columns}
+\end{frame}
+
+\section{Conclusions}
+
+% =========================================================================
+% CONCLUSION
+% =========================================================================
+\begin{frame}{Yes, with boundaries: margin integrity \alert{is defensible} under agentic orchestration}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.32\textwidth}
+ \begin{block}{SQ1\;\;distinguishability}
+ \centering
+ kernels are separable\\$p<0.001$
+ \end{block}
+ \column{0.32\textwidth}
+ \begin{block}{SQ2\;\;theoretical impact}
+ \centering
+ COI erosion mechanism\\proved in baseline limit
+ \end{block}
+ \column{0.32\textwidth}
+ \begin{block}{SQ3\;\;mitigation}
+ \centering
+ robust control shifts\\COI / revenue / UX trade-off
+ \end{block}
+ \end{columns}
+
+ \vspace{0.5em}
+ \begin{alertblock}{Boundary conditions}
+ Evidence is from a controlled platform and a small labeled cohort.
+ This is mechanism validation, not full production external validity.
+ \end{alertblock}
+\end{frame}
+
+% =========================================================================
+% IMPLICATIONS
+% =========================================================================
+\begin{frame}{What this implies for real pricing systems}
+ \begin{itemize}\setlength{\itemsep}{0.7em}
+ \item \textbf{Financially:} untreated reconnaissance behaves like an information leak and can compress sustainable margins.
+ \item \textbf{Operationally:} behavior-only session scoring can be wired into pricing without device fingerprinting.
+ \item \textbf{Market exposure:} channels where dynamic pricing is a secondary layer (aggregators, comparison funnels, promo traffic) are disrupted first.
+ \item \textbf{Strategically:} robust pricing should be calibrated by regime; there is no single penalty that wins everywhere.
+ \item \textbf{Before deployment:} larger human baselines, governance review, and legal safeguards are mandatory.
+ \end{itemize}
+\end{frame}
+
+% =========================================================================
+% THANK YOU
+% =========================================================================
+\begin{frame}[plain]
+ \centering
+ \vfill
+ {\LARGE\bfseries Thank you}
+ \vspace{0.8em}
+
+ {\large Questions and discussion}
+
+ \vfill
+ {\footnotesize\color{PhantomSlate}Appendix follows: COI theorem derivation, reward composition, and sample-size notes.}
+ \vfill
+\end{frame}
+
+\appendix
+\input{defense_appendix}
+
+\end{document}
diff --git a/paper/defense/defense_appendix.tex b/paper/defense/defense_appendix.tex
new file mode 100644
index 0000000..460c7d3
--- /dev/null
+++ b/paper/defense/defense_appendix.tex
@@ -0,0 +1,364 @@
+% Included by defense.tex after the main deck (extensive appendix).
+
+\section{Appendix}
+
+\begin{frame}{Appendix roadmap}
+ \footnotesize
+ \begin{columns}[T,onlytextwidth]
+ \column{0.31\textwidth}
+ \begin{block}{A.\ Objects}
+ Notation, COI, proxies
+ \end{block}
+ \column{0.31\textwidth}
+ \begin{block}{B.\ Mechanism}
+ Order stats, kernels, KL
+ \end{block}
+ \column{0.31\textwidth}
+ \begin{block}{C.\ Control}
+ Simulator, robust loop, factorial grid
+ \end{block}
+ \end{columns}
+ \vfill
+ \begin{alertblock}{Figures}
+ Full charts, MDPs, extra revenue view
+ \end{alertblock}
+\end{frame}
+
+% ----- A. Notation & definitions -----
+
+\begin{frame}{Appendix: core notation (quick reference, I)}
+ \scriptsize
+ \begin{align*}
+ \tau_s &= (e_{s,1},\ldots,e_{s,L_s}) && \text{session} \\
+ \hat{q}_{t,i} &= \sum_{s\in S_t}\sum_k \omega(a_{s,k})\,\mathbf{1}[i_{s,k}=i] && \text{proxy }(\humanicon, \roboticon) \\
+ Q(p) &= (1-\alpha)\,\mathbb{E}_{\theta\sim D_H}[d(p;\theta)] \\
+ &\quad + \alpha\,\mathbb{E}_{\theta\sim D_A}[d(p;\theta)] + \epsilon_t && \text{mixture of }\humanicon/\roboticon \\
+ \mathrm{COI}(\pi) &= \mathbb{E}[P]-\underline{p} && \text{COI}
+ \end{align*}
+\end{frame}
+
+\begin{frame}{Appendix: core notation (quick reference, II)}
+ \footnotesize
+ \begin{itemize}
+ \item \(\underline{p}\): minimum viable price anchor (thesis simplification).
+ \item \(\alpha\): contamination with agent traffic in the mixture.
+ \item \(\omega(a)\): hand-engineered action weights for the proxy (baseline).
+ \end{itemize}
+ \begin{alertblock}{Reading guide}
+ Objects on the left are \textbf{observable}; \(d(\cdot)\) and many \(\theta\) remain hidden.
+ \end{alertblock}
+\end{frame}
+
+\begin{frame}{Appendix: COI as a reporting functional}
+ \[
+ \mathrm{COI}(\pi) = \mathbb{E}_{P\sim F_\pi}[P] - \underline{p}
+ \]
+ \begin{block}{Interpretation}
+ Premium above the floor induced by policy \(\pi\); used as a KPI and as the object Theorem 1 attacks under query saturation.
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: demand proxy vs.\ latent demand}
+ \[
+ \hat{q}_{t,i}=\sum_{s\in S_t}\sum_{k=1}^{L_s} \omega(a_{s,k})\,\mathbf{1}[i_{s,k}=i]
+ \]
+ \begin{alertblock}{Key distinction}
+ \(\hat{q}\) is an operational sensor from logs (\humanicon, \roboticon); true demand \(d(p;\theta)\) stays latent. Pricing reacts to \(\hat{q}\), so agent-shaped behavior can poison the signal.
+ \end{alertblock}
+\end{frame}
+
+% ----- B. Mechanism -----
+
+\begin{frame}{Appendix: independent draws and order statistics (intuition)}
+ \begin{columns}[T]
+ \column{0.55\textwidth}
+ \begin{itemize}
+ \item Independent price draws \(\{P_i\}_{i=1}^N\) from fixed offer law.
+ \item Purchase-side minimum behaves like \(P_{(1)}\): mass shifts left as \(N\) grows.
+ \item Expected premium vs.\ \(\underline{p}\) compresses: COI pressure.
+ \end{itemize}
+ \column{0.42\textwidth}
+ \centering
+ \begin{tikzpicture}[scale=0.85]
+ \draw[->,thick] (0,0)--(3.2,0) node[right] {\small queries \(N\)};
+ \draw[->,thick] (0,0)--(0,2.2) node[above] {\small COI};
+ \draw[PhantomCyan,very thick] (0.2,2) .. controls (1.5,1.2) and (2.2,0.5) .. (3,0.15);
+ \node[below right] at (2.4,0.6) {\footnotesize saturation};
+ \end{tikzpicture}
+ \end{columns}
+\end{frame}
+
+\begin{frame}{Appendix: Theorem 1 scope (what is and is not claimed)}
+ \small
+ \begin{block}{Inside the baseline proof}
+ Non-collusive sessions, independent draws, fixed offer distribution across queries.
+ \end{block}
+ \begin{alertblock}{Outside (handled elsewhere)}
+ Collusion, pooled recon, sequential repricing that breaks iid structure: evidence moves to the simulator.
+ \end{alertblock}
+\end{frame}
+
+\begin{frame}{Appendix: empirical transition kernel (MLE)}
+ \[
+ \hat{P}(s'\mid s)=\frac{N(s,s')}{\sum_k N(s,k)}
+ \]
+ \begin{block}{Use}
+ Human and agent centroids \(\bar{T}_H,\bar{T}_A\) for divergence-to-prototype scores.
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: KL to prototypes (shared support)}
+ \[
+ \Delta_H = D_{\mathrm{KL}}(\hat{T}'\,\|\,\bar{T}_H),\qquad
+ \Delta_A = D_{\mathrm{KL}}(\hat{T}'\,\|\,\bar{T}_A)
+ \]
+ \begin{exampleblock}{Asymmetric choice}
+ KL measures deviation from the \textbf{human} reference; symmetric JS/Wasserstein on behavior was not the design target.
+ \end{exampleblock}
+\end{frame}
+
+\begin{frame}{Appendix: softmax to sigmoid (algebra)}
+ \small
+ Let \(z_A=-\Delta_A/T\), \(z_H=-\Delta_H/T\). Then
+ \begin{align*}
+ P(A\mid\tau) &= \frac{e^{z_A}}{e^{z_A}+e^{z_H}}
+ = \frac{1}{1+e^{z_H-z_A}}
+ = \sigma\bigl(z_A-z_H\bigr) \\
+ &= \sigma\!\left(\frac{\Delta_H-\Delta_A}{T}\right).
+ \end{align*}
+ \begin{block}{Takeaway}
+ Two-class softmax over \((z_A,z_H)\) is exactly one sigmoid on the gap \((\Delta_H-\Delta_A)\).
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: contamination generator \(\mathcal{G}(\alpha)\)}
+ \[
+ \mathcal{G}(\alpha):\ \text{inject synthetic agent trajectories until mixture reaches target }\alpha
+ \]
+ \begin{alertblock}{Role in the lab}
+ Supplies controlled stress tests for the pricing learner; not a claim of production-faithful agents.
+ \end{alertblock}
+\end{frame}
+
+% ----- C. Robust control -----
+
+\begin{frame}{Appendix: Wasserstein ambiguity (ideal object)}
+ \[
+ \mathcal{U}_\epsilon(\hat{P}_N)=\left\{ Q:\ W_p(Q,\hat{P}_N)\le \epsilon \right\}
+ \]
+ \begin{block}{What the code implements instead}
+ A \textbf{local} grid over \(\alpha\) near \(\alpha_0\) with radius \(\epsilon_\alpha\): tractable inner worst case, not a full ball solver.
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: per-step reward sketch}
+ \small
+ \[
+ r = R(p,d) - \lambda\,\mathrm{COI}_{\mathrm{leak}}(p,\tau') - \eta\,\mathrm{UX}(\tau',p) - \text{(supra-competitive excess)}
+ \]
+ \begin{itemize}
+ \item Query-tax style \(\mathrm{COI}_{\mathrm{leak}}\): minimal nonzero surrogate to expose the control channel.
+ \item UX and anchor penalties prevent trivial solutions (flat but exploitative prices).
+ \end{itemize}
+\end{frame}
+
+\begin{frame}{Appendix: factorial design (192 cells)}
+ \footnotesize
+ \centering
+ \begin{tabular}{@{}llr@{}}
+ \toprule
+ Axis & Levels & Count \\
+ \midrule
+ RL algorithm & PPO, A2C, DQN, Q-table & 4 \\
+ Contamination \(\alpha\) & 4 representative values in \([0.1,0.6]\) & 4 \\
+ Robustness radius \(\epsilon_\alpha\) & 3 & 3 \\
+ COI penalty \(\lambda_{\mathrm{coi}}\) & 2 & 2 \\
+ Action granularity & 2 & 2 \\
+ \midrule
+ \textbf{Total} & & \(4\times4\times3\times2\times2=\mathbf{192}\) \\
+ \bottomrule
+ \end{tabular}
+\end{frame}
+
+\begin{frame}{Appendix: engineering note (pandas \(\to\) JAX)}
+ \begin{itemize}
+ \item Hot path was label-indexed transition lookups; profiling showed pandas overhead dominated.
+ \item Integer-indexed arrays + JAX inner loop: large step/s throughput (thesis numbers; environment dependent).
+ \item Kronecker expansion of product-conditioned kernels: research simulator cost, scales with catalog.
+ \end{itemize}
+\end{frame}
+
+% ----- Extended figures (all PDFs in repo) -----
+
+\begin{frame}{Appendix figure: COI by \(\alpha\) (full)}
+ \centering
+ \includegraphics[width=0.92\linewidth,height=0.78\textheight,keepaspectratio]{final_focus_coi_by_alpha.pdf}
+\end{frame}
+
+\begin{frame}{Appendix figure: revenue deltas (full)}
+ \centering
+ \includegraphics[width=0.92\linewidth,height=0.78\textheight,keepaspectratio]{final_focus_revenue_delta.pdf}
+\end{frame}
+
+\begin{frame}{Appendix figure: revenue by \(\alpha\) (full)}
+ \centering
+ \includegraphics[width=0.92\linewidth,height=0.78\textheight,keepaspectratio]{final_focus_revenue_by_alpha.pdf}
+\end{frame}
+
+\begin{frame}{Appendix figure: risk / stability deltas (full)}
+ \centering
+ \includegraphics[width=0.92\linewidth,height=0.78\textheight,keepaspectratio]{final_focus_risk_deltas.pdf}
+\end{frame}
+
+\begin{frame}{Appendix figure: COI preservation grid (full)}
+ \centering
+ \includegraphics[width=0.92\linewidth,height=0.78\textheight,keepaspectratio]{final_focus_coi_preservation_grid.pdf}
+\end{frame}
+
+\begin{frame}{Appendix figure: human MDP (full)}
+ \centering
+ \includegraphics[width=0.75\linewidth,height=0.82\textheight,keepaspectratio]{mdp_human.pdf}
+\end{frame}
+
+\begin{frame}{Appendix figure: agent MDP (full)}
+ \centering
+ \includegraphics[width=0.75\linewidth,height=0.82\textheight,keepaspectratio]{mdp_agent.pdf}
+\end{frame}
+
+% ----- Threat model & evaluation -----
+
+\begin{frame}{Appendix: threat model map}
+ \centering
+ \resizebox{0.98\linewidth}{!}{%
+ \begin{tikzpicture}[
+ font=\sffamily\footnotesize,
+ box/.style={draw=PhantomInk,rounded corners=2pt,thick,align=center,inner sep=5pt,minimum width=2.8cm},
+ arr/.style={-Stealth,thick,PhantomSlate}
+ ]
+ \node[box,fill=PhantomCyan!18] (A) at (0,0) {\textbf{Focus}\\[0.15em]browser agents\\into \(\hat{q}\)};
+ \node[box,fill=white] (B) at (3.8,0) {\textbf{Complementary}\\[0.15em]WAF, CAPTCHA,\\rate limits};
+ \node[box,fill=white] (C) at (7.6,0) {\textbf{Upstream}\\[0.15em]API scrape,\\no UI semantics};
+ \draw[arr] (A) -- node[above] {\tiny scope} (B);
+ \draw[arr] (B) -- node[above] {\tiny out of scope} (C);
+ \end{tikzpicture}%
+ }
+ \vfill
+ \begin{block}{Claim boundary}
+ Residual contamination after security controls is the motivating scenario.
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: evaluation checklist (robustness culture)}
+ \footnotesize
+ \begin{enumerate}
+ \item Session-aware labels: avoid splitting rows inside a trajectory if that inflates scores.
+ \item Document how prototypes \(\bar{T}_H,\bar{T}_A\) were fit (full cohort vs.\ held-out); state explicitly in writing.
+ \item Report temperature \(T\) as calibration, not as a tuned hyperparameter unless a sweep is shown.
+ \item Separate \textbf{architecture} claims from \textbf{coverage} claims (hotel vs.\ airline balance at release).
+ \end{enumerate}
+\end{frame}
+
+\begin{frame}{Appendix: sim-to-real gap (explicit)}
+ \begin{itemize}
+ \item Kernels and generators reflect a \textbf{small labeled cohort} and a \textbf{browser-use style} agent class.
+ \item RL policies are trained in a \textbf{surrogate} market with engineered rewards and discretized prices.
+ \item Deployment would require legal review, fairness testing, and refreshed baselines at scale.
+ \end{itemize}
+\end{frame}
+
+\begin{frame}{Appendix: leakage surrogate (query-tax form)}
+ \small
+ \[
+ \mathrm{COI}_{\mathrm{leak}}(p,\tau') \approx f(\tau')\cdot c_{\mathrm{info}}
+ \]
+ \begin{block}{Reading}
+ \(f(\tau')\) is the weak agent score; \(c_{\mathrm{info}}\) is a minimal constant leakage proxy to expose the control channel. Revelation-style \(-\log \pi(p\mid\tau')\) is the natural upgrade.
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: robust pricing template (symbolic)}
+ \footnotesize
+ \[
+ \max_\pi\ \min_{Q\in\mathcal{U}_\epsilon(\hat{P}_N)} \mathbb{E}_{d\sim Q}\bigl[ R(p,d) - \lambda\,\mathrm{COI}_{\mathrm{leak}} - \eta\,\mathrm{UX} \bigr]
+ \]
+ \begin{alertblock}{Code-level substitute}
+ Inner min over a \textbf{finite grid} of \(\alpha_k\in[\alpha_0\pm\epsilon_\alpha]\) around the nominal generator mix, not a continuous adversary over all \(Q\) in the ball.
+ \end{alertblock}
+\end{frame}
+
+\begin{frame}{Appendix: why a Stackelberg game is a useful abstraction}
+ \footnotesize
+ \begin{columns}[T,onlytextwidth]
+ \column{0.52\textwidth}
+ \begin{itemize}
+ \item \textbf{Leader move}: the platform commits a quote via policy \(p_t=\pi(x_t)\).
+ \item \textbf{Follower move}: session behavior then reacts (click, continue, abandon, purchase).
+ \item This ordering matches real serving APIs: price is emitted before response is observed.
+ \item Repeating this local sequence gives a tractable leader-follower control model.
+ \end{itemize}
+
+ \column{0.44\textwidth}
+ \centering
+ \begin{tikzpicture}[
+ font=\scriptsize\sffamily,
+ box/.style={draw=PhantomInk,rounded corners=4pt,minimum width=3.45cm,minimum height=0.9cm,align=center},
+ arr/.style={-{Stealth[length=2.0mm]},thick,PhantomSlate}
+ ]
+ \node[box,fill=PhantomCyan!14] (l) at (0,1.2) {Leader: pricing policy};
+ \node[box,fill=white] (f) at (0,-0.05) {Follower: session response};
+ \node[box,fill=PhantomIndigo!10] (u) at (0,-1.3) {State update \& next round};
+ \draw[arr] (l) -- node[right,font=\tiny] {quote} (f);
+ \draw[arr] (f) -- node[right,font=\tiny] {events} (u);
+ \draw[arr] (u.west) to[bend left=35] node[left,font=\tiny] {context} (l.west);
+ \end{tikzpicture}
+ \end{columns}
+ \begin{alertblock}{Boundary}
+ We do \textbf{not} claim a full market equilibrium. We claim a useful timing model for explainable policy updates under contamination.
+ \end{alertblock}
+\end{frame}
+
+\begin{frame}{Appendix: why Theorem 1 helps (without over-claiming)}
+ \footnotesize
+ \begin{columns}[T,onlytextwidth]
+ \column{0.48\textwidth}
+ \begin{block}{What the theorem gives us}
+ \begin{itemize}
+ \item A directional mechanism: independent recon pressure compresses COI.
+ \item A sanity check for reward design: leakage penalties should grow with recon likelihood.
+ \item A clean explanatory anchor for stakeholders and governance review.
+ \end{itemize}
+ \end{block}
+
+ \column{0.48\textwidth}
+ \begin{alertblock}{What the theorem does not claim}
+ \begin{itemize}
+ \item It is not a finite-sample forecast for every market.
+ \item It does not cover collusion or all adaptive adversaries.
+ \item It does not replace simulator evidence or offline policy validation.
+ \end{itemize}
+ \end{alertblock}
+ \end{columns}
+
+ \vspace{0.2em}
+ \begin{block}{Three evidence layers used in this thesis}
+ \textbf{Theorem 1} (mechanism direction) \(\rightarrow\) \textbf{simulator} (finite-regime quantification) \(\rightarrow\) \textbf{implementation} (local robust policy training).
+ \end{block}
+\end{frame}
+
+\begin{frame}{Appendix: composite strip (five plots, small multiples)}
+ \centering
+ {\footnotesize\itshape Same PDFs as the main talk, shrunk to scan the full panel at once.\par}
+ \vspace{0.25em}
+ \begin{columns}[T,onlytextwidth]
+ \column{0.19\textwidth}
+ \includegraphics[width=\linewidth,height=0.26\textheight,keepaspectratio]{final_focus_coi_by_alpha.pdf}
+ \column{0.19\textwidth}
+ \includegraphics[width=\linewidth,height=0.26\textheight,keepaspectratio]{final_focus_revenue_delta.pdf}
+ \column{0.19\textwidth}
+ \includegraphics[width=\linewidth,height=0.26\textheight,keepaspectratio]{final_focus_revenue_by_alpha.pdf}
+ \column{0.19\textwidth}
+ \includegraphics[width=\linewidth,height=0.26\textheight,keepaspectratio]{final_focus_risk_deltas.pdf}
+ \column{0.19\textwidth}
+ \includegraphics[width=\linewidth,height=0.26\textheight,keepaspectratio]{final_focus_coi_preservation_grid.pdf}
+ \end{columns}
+\end{frame}
diff --git a/paper/defense/manim/.gitignore b/paper/defense/manim/.gitignore
new file mode 100644
index 0000000..9f4a27f
--- /dev/null
+++ b/paper/defense/manim/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/
+*.pyc
+media/
diff --git a/paper/defense/manim/common.py b/paper/defense/manim/common.py
new file mode 100644
index 0000000..33b42be
--- /dev/null
+++ b/paper/defense/manim/common.py
@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+from typing import Iterable
+
+import numpy as np
+from manim import (
+ Arrow,
+ BLUE_D,
+ CurvedArrow,
+ DOWN,
+ DashedLine,
+ GREEN_C,
+ GREY_B,
+ LEFT,
+ Line,
+ MathTex,
+ Matrix,
+ RIGHT,
+ RoundedRectangle,
+ SurroundingRectangle,
+ Text,
+ UP,
+ VGroup,
+ config,
+)
+
+P_MIN = 80.0
+P_MAX = 160.0
+LIGHT_BG = "#F8F8F4"
+INK = "#1E1E1E"
+AXIS_INK = "#2C2C2C"
+HIGHLIGHT = "#8F5F00"
+
+config.background_color = LIGHT_BG
+Text.set_default(color=INK)
+MathTex.set_default(color=INK)
+Line.set_default(color=AXIS_INK)
+Arrow.set_default(color=AXIS_INK)
+CurvedArrow.set_default(color=AXIS_INK)
+DashedLine.set_default(color=AXIS_INK)
+
+
+def normal_pdf(x: float, mu: float, sigma: float) -> float:
+ z = (x - mu) / sigma
+ return float(np.exp(-0.5 * z * z) / (sigma * np.sqrt(2.0 * np.pi)))
+
+
+def scene_title(text: str) -> Text:
+ return Text(text, font_size=44, weight="BOLD", color=INK).to_edge(UP)
+
+
+def card(
+ label: str,
+ color: str = BLUE_D,
+ width: float = 3.3,
+ height: float = 1.15,
+ font_size: float = 24,
+) -> VGroup:
+ box = RoundedRectangle(corner_radius=0.15, width=width, height=height)
+ box.set_stroke(color=color, width=2.0)
+ box.set_fill(color=color, opacity=0.12)
+ text = Text(label, font_size=font_size).move_to(box.get_center())
+ return VGroup(box, text)
+
+
+def to_matrix(
+ values: Iterable[Iterable[float]],
+ title: str,
+ color: str,
+ header_buff: float = 0.28,
+ fmt: str = ".2f",
+) -> VGroup:
+ mat = Matrix(
+ [[f"{v:{fmt}}" for v in row] for row in values], h_buff=1.15, v_buff=0.75
+ )
+ header = Text(title, font_size=25, weight="BOLD", color=color).next_to(
+ mat, UP, buff=header_buff
+ )
+ frame = SurroundingRectangle(mat, color=color, buff=0.2)
+ return VGroup(header, frame, mat)
+
+
+def rank_from_scale(scale: int) -> str:
+ clamped = max(1, min(scale, 10))
+ return "A" if clamped == 1 else str(clamped)
+
+
+def actor_face_card(
+ rank: str,
+ role: str,
+ accent: str,
+ width: float = 1.6,
+ height: float = 2.25,
+ show_role: bool = True,
+) -> VGroup:
+ frame = RoundedRectangle(corner_radius=0.1, width=width, height=height)
+ frame.set_stroke(color=AXIS_INK, width=2.0)
+ frame.set_fill(color="#FFFFFF", opacity=1.0)
+
+ top_rank = Text(rank, font_size=30, color=accent).move_to(
+ frame.get_corner(UP + LEFT) + RIGHT * 0.2 + DOWN * 0.22
+ )
+ bottom_rank = (
+ Text(rank, font_size=30, color=accent)
+ .rotate(np.pi)
+ .move_to(frame.get_corner(DOWN + RIGHT) + LEFT * 0.2 + UP * 0.22)
+ )
+ center_rank = Text(rank, font_size=56, weight="BOLD", color=accent).move_to(
+ frame.get_center() + UP * 0.03
+ )
+
+ parts = [frame, top_rank, bottom_rank, center_rank]
+ if show_role:
+ role_label = Text(role, font_size=18, color=GREY_B).next_to(
+ frame, DOWN, buff=0.08
+ )
+ parts.append(role_label)
+ return VGroup(*parts)
+
+
+def product_suit_card(
+ suit: str,
+ scale: int,
+ accent: str,
+ width: float = 1.86,
+ height: float = 1.04,
+ show_label: bool = False,
+) -> tuple[VGroup, Text]:
+ frame = RoundedRectangle(corner_radius=0.08, width=width, height=height)
+ frame.set_stroke(color=AXIS_INK, width=2.0)
+ frame.set_fill(color="#FFFFFF", opacity=1.0)
+
+ suit_left = Text(suit, font_size=28, color=accent).move_to(
+ frame.get_left() + RIGHT * 0.22
+ )
+ suit_right = Text(suit, font_size=28, color=accent).move_to(
+ frame.get_right() + LEFT * 0.22
+ )
+ scale_text = Text(
+ rank_from_scale(scale),
+ font_size=40,
+ weight="BOLD",
+ color=accent,
+ ).move_to(frame.get_center())
+
+ parts = [frame, suit_left, suit_right, scale_text]
+ if show_label:
+ scale_label = Text("scale", font_size=14, color=GREY_B).next_to(
+ frame, DOWN, buff=0.04
+ )
+ parts.append(scale_label)
+ return VGroup(*parts), scale_text
+
+
+def private_valuation_card(value: int, show_label: bool = False) -> VGroup:
+ frame = RoundedRectangle(corner_radius=0.08, width=1.86, height=1.04)
+ frame.set_stroke(color=AXIS_INK, width=2.0)
+ frame.set_fill(color="#FFFFFF", opacity=1.0)
+
+ rank = Text(
+ rank_from_scale(value), font_size=40, weight="BOLD", color=GREEN_C
+ ).move_to(frame.get_center())
+ left_tag = Text("v", font_size=28, color=INK).move_to(
+ frame.get_left() + RIGHT * 0.22
+ )
+ right_tag = Text("*", font_size=28, color=INK).move_to(
+ frame.get_right() + LEFT * 0.22
+ )
+
+ parts = [frame, left_tag, right_tag, rank]
+ if show_label:
+ title = Text("private value", font_size=14, color=GREY_B).next_to(
+ frame, DOWN, buff=0.04
+ )
+ parts.append(title)
+ return VGroup(*parts)
diff --git a/paper/defense/manim/defense.py b/paper/defense/manim/defense.py
new file mode 100644
index 0000000..20e33b6
--- /dev/null
+++ b/paper/defense/manim/defense.py
@@ -0,0 +1,23 @@
+"""Manim entry module only.
+
+Scene implementations are in scenes/main.py and scenes/appendix.py. Manim names
+output folders after the file you pass to the CLI; pointing everything at this
+file keeps all MP4s under media/videos/defense/ instead of splitting by source file.
+"""
+
+from __future__ import annotations
+
+import importlib
+
+from manim import Scene
+
+_modname = __name__
+
+for _mod in ("scenes.main", "scenes.appendix"):
+ m = importlib.import_module(_mod)
+ for _name, _val in list(vars(m).items()):
+ if _name.startswith("_"):
+ continue
+ if isinstance(_val, type) and issubclass(_val, Scene) and _val is not Scene:
+ _val.__module__ = _modname
+ globals()[_name] = _val
diff --git a/paper/defense/manim/defense_scene_order.txt b/paper/defense/manim/defense_scene_order.txt
new file mode 100644
index 0000000..8c26543
--- /dev/null
+++ b/paper/defense/manim/defense_scene_order.txt
@@ -0,0 +1,14 @@
+# One scene name per line; order matches `python src/render.py --group final-full`.
+# Used by scripts/ffmpeg_concat_defense.sh after rendering.
+DefenseOpening
+CardMarketAnalogyScene
+COIFirstPrinciplesScene
+COIOrderStatisticProofScene
+BehaviorKernelConstructionScene
+SeparabilitySignalScene
+ContaminationGeneratorScene
+RewardAndLeakageScene
+StackelbergAmbiguityScene
+RobustControlScene
+SystemLoopScene
+ObjectiveAndResultsScene
diff --git a/paper/defense/manim/project.json b/paper/defense/manim/project.json
new file mode 100644
index 0000000..cf858e1
--- /dev/null
+++ b/paper/defense/manim/project.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "name": "manim",
+ "projectType": "application",
+ "sourceRoot": "paper/defense/manim",
+ "targets": {
+ "render": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "bash -c 'source ../.venv/bin/activate && PYTHONPATH=. python render.py'",
+ "cwd": "paper/defense/manim"
+ }
+ },
+ "render-all": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "bash -c 'source ../.venv/bin/activate && PYTHONPATH=. python render.py --all'",
+ "cwd": "paper/defense/manim"
+ }
+ },
+ "render-full": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "bash -c 'source ../.venv/bin/activate && PYTHONPATH=. python render.py --group final-full'",
+ "cwd": "paper/defense/manim"
+ }
+ },
+ "render-poster": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "bash -c 'source ../.venv/bin/activate && PYTHONPATH=. python render.py --group poster'",
+ "cwd": "paper/defense/manim"
+ }
+ },
+ "render-appendix": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "bash -c 'source ../.venv/bin/activate && PYTHONPATH=. python render.py --group behavior-appendix && PYTHONPATH=. python render.py --group coi-appendix'",
+ "cwd": "paper/defense/manim"
+ }
+ }
+ },
+ "tags": [
+ "scope:presentation",
+ "type:manim"
+ ]
+}
diff --git a/paper/defense/manim/render.py b/paper/defense/manim/render.py
new file mode 100644
index 0000000..bb52199
--- /dev/null
+++ b/paper/defense/manim/render.py
@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+import argparse
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+from scenes.appendix import BEHAVIOR_SCENES, COI_SCENES
+from scenes.main import POSTER_SCENES, SCENE_ORDER as MAIN_SCENES
+
+# ---------------------------------------------------------------------------
+# Batch render: groups are just ordered lists of scene class names.
+# Every scene is rendered via defense.py so outputs stay in media/videos/defense/.
+# Scene code itself lives in scenes/main.py and scenes/appendix.py.
+# ---------------------------------------------------------------------------
+
+
+def _ordered_unique(items: list[str]) -> list[str]:
+ seen: set[str] = set()
+ return [item for item in items if not (item in seen or seen.add(item))]
+
+
+FINAL_CORE = [
+ "DefenseOpening",
+ "CardMarketAnalogyScene",
+ "COIFirstPrinciplesScene",
+ "COIOrderStatisticProofScene",
+ "BehaviorKernelConstructionScene",
+ "SeparabilitySignalScene",
+ "ContaminationGeneratorScene",
+ "RewardAndLeakageScene",
+ "StackelbergAmbiguityScene",
+ "RobustControlScene",
+ "SystemLoopScene",
+ "ObjectiveAndResultsScene",
+]
+
+SCENE_GROUPS: dict[str, list[str]] = {
+ "poster": list(POSTER_SCENES),
+ "final-core": FINAL_CORE,
+ "final-full": list(MAIN_SCENES),
+ "behavior-appendix": list(BEHAVIOR_SCENES),
+ "coi-appendix": list(COI_SCENES),
+}
+
+SCENE_GROUPS["all"] = _ordered_unique(
+ [
+ *SCENE_GROUPS["final-full"],
+ *SCENE_GROUPS["poster"],
+ *SCENE_GROUPS["behavior-appendix"],
+ *SCENE_GROUPS["coi-appendix"],
+ ]
+)
+
+ENTRY = "defense.py"
+SCENE_TO_FILE: dict[str, str] = {name: ENTRY for name in SCENE_GROUPS["all"]}
+
+DEFAULT_GROUP = "final-core"
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description=(
+ "Batch-render scenes. Code: scenes/main.py + scenes/appendix.py. "
+ "Manim entry: defense.py. Output: media/videos/defense//"
+ )
+ )
+ parser.add_argument(
+ "--quality",
+ default="qm",
+ choices=["ql", "qm", "qh", "qk"],
+ help="Manim quality preset",
+ )
+ selection = parser.add_mutually_exclusive_group()
+ selection.add_argument(
+ "--scene",
+ action="append",
+ dest="scenes",
+ help="Scene name; repeat to render many",
+ )
+ selection.add_argument(
+ "--group",
+ choices=sorted(SCENE_GROUPS.keys()),
+ default=DEFAULT_GROUP,
+ help=f"Named list of scenes (default: {DEFAULT_GROUP})",
+ )
+ selection.add_argument("--all", action="store_true", help="Render every scene")
+ parser.add_argument(
+ "--media-dir",
+ default="media",
+ help="Relative to this folder (default: media)",
+ )
+ parser.add_argument("--preview", action="store_true", help="Open each video")
+ parser.add_argument("--list", action="store_true", help="Print groups and exit")
+ return parser.parse_args()
+
+
+def validate_requested(requested: list[str]) -> list[str]:
+ missing = [name for name in requested if name not in SCENE_TO_FILE]
+ if missing:
+ choices = ", ".join(SCENE_TO_FILE.keys())
+ raise ValueError(
+ f"Unknown scenes: {', '.join(missing)}\nAvailable choices: {choices}"
+ )
+ return requested
+
+
+def resolve_scenes(args: argparse.Namespace) -> list[str]:
+ if args.all:
+ return list(SCENE_GROUPS["all"])
+ if args.scenes:
+ return validate_requested(args.scenes)
+ return list(SCENE_GROUPS[args.group])
+
+
+def run_manim(
+ scene_file: Path,
+ scene_name: str,
+ quality: str,
+ preview: bool,
+ working_dir: Path,
+ media_dir: str,
+ pythonpath: str,
+) -> None:
+ env = os.environ.copy()
+ prev = env.get("PYTHONPATH")
+ env["PYTHONPATH"] = pythonpath if not prev else f"{pythonpath}:{prev}"
+
+ cmd = [sys.executable, "-m", "manim"]
+ if preview:
+ cmd.append("-p")
+ cmd.extend(["--media_dir", media_dir])
+ cmd.extend([f"-{quality}", str(scene_file), scene_name])
+ subprocess.run(cmd, cwd=working_dir, check=True, env=env)
+
+
+def main() -> int:
+ args = parse_args()
+ if args.list:
+ for group_name in sorted(SCENE_GROUPS):
+ print(f"[{group_name}]")
+ for scene in SCENE_GROUPS[group_name]:
+ print(f" {scene}")
+ return 0
+
+ root = Path(__file__).resolve().parent
+ py_path = str(root)
+ names = resolve_scenes(args)
+
+ try:
+ for scene_name in names:
+ scene_file = root / SCENE_TO_FILE[scene_name]
+ run_manim(
+ scene_file=scene_file,
+ scene_name=scene_name,
+ quality=args.quality,
+ preview=args.preview,
+ working_dir=root,
+ media_dir=args.media_dir,
+ pythonpath=py_path,
+ )
+ except FileNotFoundError:
+ print("manim not found.", file=sys.stderr)
+ return 2
+ except ValueError as exc:
+ print(str(exc), file=sys.stderr)
+ return 2
+ except subprocess.CalledProcessError as exc:
+ return exc.returncode
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/paper/defense/manim/render_defense b/paper/defense/manim/render_defense
new file mode 100755
index 0000000..750423a
--- /dev/null
+++ b/paper/defense/manim/render_defense
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+# Render thesis-defense Manim clips. Run from anywhere (script cd's to its dir).
+#
+# ./render_defense # main reel: final-full, medium quality
+# ./render_defense --quality qh # high quality for recording
+# ./render_defense core # shorter committee cut (final-core)
+# ./render_defense all # everything: main + poster + both appendices
+# ./render_defense appendix # behavior + COI appendix only
+# ./render_defense poster
+# ./render_defense list
+# ./render_defense --scene DefenseOpening --scene CardMarketAnalogyScene
+#
+# Env: MANIM_PYTHON=/path/to/python overrides auto-detected venv.
+
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")" && pwd)"
+cd "$ROOT"
+
+if [[ -n "${MANIM_PYTHON:-}" ]]; then
+ PY="$MANIM_PYTHON"
+elif [[ -x "$ROOT/../.venv/bin/python" ]]; then
+ PY="$ROOT/../.venv/bin/python"
+else
+ PY="$(command -v python3 2>/dev/null || command -v python)"
+fi
+
+if [[ ! -x "$PY" ]] && ! command -v "$PY" &>/dev/null; then
+ echo "No Python found. Set MANIM_PYTHON or create paper/defense/.venv" >&2
+ exit 1
+fi
+
+export PYTHONPATH="$ROOT"
+
+run() {
+ "$PY" "$ROOT/render.py" "$@"
+}
+
+CMD=full
+case "${1-}" in
+ full|core|all|appendix|poster|list|help|-h|--help)
+ CMD="$1"
+ shift
+ ;;
+esac
+
+case "$CMD" in
+ help|-h|--help)
+ cat <<'EOF'
+Render thesis-defense Manim clips (cd to paper/defense/manim is automatic).
+
+ ./render_defense main reel (final-full), default quality qm
+ ./render_defense --quality qh same, high quality for recording
+ ./render_defense core shorter cut (final-core)
+ ./render_defense all main + poster + both appendices
+ ./render_defense appendix behavior-appendix + coi-appendix
+ ./render_defense poster
+ ./render_defense list scene names and source files
+ ./render_defense --scene Name [--scene Name2 ...]
+
+Env MANIM_PYTHON overrides Python (default: ../.venv/bin/python next to this dir).
+EOF
+ exit 0
+ ;;
+ list)
+ run --list "$@"
+ exit 0
+ ;;
+ full)
+ run --group final-full "$@"
+ ;;
+ core)
+ run --group final-core "$@"
+ ;;
+ all)
+ run --all "$@"
+ ;;
+ appendix)
+ run --group behavior-appendix "$@"
+ run --group coi-appendix "$@"
+ ;;
+ poster)
+ run --group poster "$@"
+ ;;
+ *)
+ echo "Unknown command: $CMD" >&2
+ exit 1
+ ;;
+esac
diff --git a/paper/defense/manim/scenes/__init__.py b/paper/defense/manim/scenes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/paper/defense/manim/scenes/appendix.py b/paper/defense/manim/scenes/appendix.py
new file mode 100644
index 0000000..d1e9829
--- /dev/null
+++ b/paper/defense/manim/scenes/appendix.py
@@ -0,0 +1,670 @@
+from __future__ import annotations
+
+import numpy as np
+from manim import *
+from common import AXIS_INK, HIGHLIGHT, INK, P_MAX, P_MIN, card, normal_pdf, scene_title, to_matrix
+
+
+class LogsToKernelsScene(Scene):
+ def construct(self):
+ title = scene_title("From Event Logs to Transition Kernels")
+ self.play(Write(title))
+
+ # 1. Logs
+ log_lines = VGroup(
+ Text('{"session": "H1", "event": "start"}', font="monospace", font_size=16),
+ Text('{"session": "A1", "event": "start"}', font="monospace", font_size=16),
+ Text('{"session": "H1", "event": "view"}', font="monospace", font_size=16),
+ Text('{"session": "A1", "event": "view"}', font="monospace", font_size=16),
+ Text(
+ '{"session": "H1", "event": "detail"}', font="monospace", font_size=16
+ ),
+ Text(
+ '{"session": "A1", "event": "detail"}', font="monospace", font_size=16
+ ),
+ Text('{"session": "H1", "event": "cart"}', font="monospace", font_size=16),
+ Text('{"session": "A1", "event": "view"}', font="monospace", font_size=16),
+ Text('{"session": "H1", "event": "buy"}', font="monospace", font_size=16),
+ Text(
+ '{"session": "A1", "event": "detail"}', font="monospace", font_size=16
+ ),
+ ).arrange(DOWN, aligned_edge=LEFT, buff=0.1)
+ log_lines.to_edge(LEFT, buff=1.0).shift(UP * 0.5)
+
+ self.play(
+ LaggedStart(
+ *[FadeIn(line, shift=UP * 0.1) for line in log_lines], lag_ratio=0.1
+ )
+ )
+ self.wait(0.5)
+
+ # 2. Nodes in a grid
+ def create_node(text, color):
+ circ = Circle(radius=0.4, color=color, fill_opacity=0.2)
+ lbl = Text(text, font_size=14).move_to(circ)
+ return VGroup(circ, lbl)
+
+ h_states = ["start", "view", "detail", "cart", "buy"]
+ a_states = ["start", "view", "detail", "view", "detail"]
+
+ h_nodes = VGroup(*[create_node(s, BLUE_D) for s in h_states]).arrange(
+ RIGHT, buff=0.5
+ )
+ a_nodes = VGroup(*[create_node(s, RED_C) for s in a_states]).arrange(
+ RIGHT, buff=0.5
+ )
+
+ trajectories = VGroup(h_nodes, a_nodes).arrange(DOWN, buff=1.0)
+ trajectories.to_edge(RIGHT, buff=1.0).shift(UP * 0.5)
+
+ h_label = Text("Human Trajectory", font_size=18, color=BLUE_D).next_to(
+ h_nodes, UP
+ )
+ a_label = Text("Agent Trajectory", font_size=18, color=RED_C).next_to(
+ a_nodes, UP
+ )
+
+ self.play(
+ ReplacementTransform(log_lines[0::2], h_nodes),
+ ReplacementTransform(log_lines[1::2], a_nodes),
+ FadeIn(h_label),
+ FadeIn(a_label),
+ )
+
+ # Add connecting lines
+ h_lines = VGroup(
+ *[
+ Line(h_nodes[i].get_right(), h_nodes[i + 1].get_left(), color=BLUE_D)
+ for i in range(len(h_nodes) - 1)
+ ]
+ )
+ a_lines = VGroup(
+ *[
+ Line(a_nodes[i].get_right(), a_nodes[i + 1].get_left(), color=RED_C)
+ for i in range(len(a_nodes) - 1)
+ ]
+ )
+
+ self.play(Create(h_lines), Create(a_lines))
+ self.wait(1)
+
+ # 3. Counts to Kernel
+ mle_text = MathTex(
+ r"\hat P(s'\mid s) = \frac{N(s,s')}{\sum_k N(s,k)}",
+ font_size=36,
+ color=HIGHLIGHT,
+ )
+ mle_text.next_to(trajectories, DOWN, buff=0.8)
+ self.play(Write(mle_text))
+
+ counts = to_matrix(
+ [
+ [0, 8, 0, 0],
+ [0, 2, 5, 1],
+ [0, 3, 2, 4],
+ [0, 1, 0, 6],
+ ],
+ "Count Matrix N",
+ color=BLUE_D,
+ fmt=".0f",
+ )
+
+ probs = to_matrix(
+ [
+ [0.00, 1.00, 0.00, 0.00],
+ [0.00, 0.25, 0.62, 0.13],
+ [0.00, 0.33, 0.22, 0.45],
+ [0.00, 0.14, 0.00, 0.86],
+ ],
+ "Kernel T",
+ color=GREEN_C,
+ )
+
+ mats = VGroup(counts, probs).arrange(RIGHT, buff=1.5).scale(0.65)
+
+ arrow = Arrow(counts.get_right(), probs.get_left(), buff=0.2)
+ arrow_lbl = MathTex(
+ r"\text{normalize}", font_size=18, color=GREY_B
+ ).next_to(arrow, UP)
+
+ # clear top half to make space if needed
+ self.play(
+ FadeOut(h_nodes),
+ FadeOut(a_nodes),
+ FadeOut(h_lines),
+ FadeOut(a_lines),
+ FadeOut(h_label),
+ FadeOut(a_label),
+ mle_text.animate.to_edge(UP, buff=1.5).set_x(0),
+ )
+ mats.next_to(mle_text, DOWN, buff=0.5)
+ arrow.move_to((counts.get_right() + probs.get_left()) / 2)
+ arrow_lbl.next_to(arrow, UP)
+
+ self.play(FadeIn(counts, shift=UP * 0.2))
+ self.play(GrowArrow(arrow), FadeIn(arrow_lbl))
+ self.play(FadeIn(probs, shift=UP * 0.2))
+ self.wait(1)
+
+
+class KLSeparabilityAndSignificanceScene(Scene):
+ def construct(self):
+ title = scene_title("Behavioral Separability & Significance")
+ self.play(Write(title))
+
+ human_mat = to_matrix(
+ [
+ [0.05, 0.70, 0.20, 0.05],
+ [0.05, 0.20, 0.60, 0.15],
+ [0.10, 0.25, 0.30, 0.35],
+ [0.00, 0.00, 0.00, 1.00],
+ ],
+ "Human Centroid T_H",
+ BLUE_D,
+ ).scale(0.7)
+
+ agent_mat = to_matrix(
+ [
+ [0.03, 0.82, 0.12, 0.03],
+ [0.06, 0.55, 0.21, 0.18],
+ [0.08, 0.48, 0.14, 0.30],
+ [0.00, 0.00, 0.00, 1.00],
+ ],
+ "Agent Centroid T_A",
+ RED_C,
+ ).scale(0.7)
+
+ centroids = VGroup(human_mat, agent_mat).arrange(RIGHT, buff=1.0)
+ centroids.next_to(title, DOWN, buff=0.5)
+ self.play(FadeIn(centroids, shift=DOWN * 0.2))
+
+ # Trajectory
+ t_prime = MathTex(r"\hat T'", font_size=36, color=HIGHLIGHT)
+ d_h = MathTex(r"\Delta_H = D_{KL}(\hat T' \parallel \bar T_H)", font_size=32)
+ d_a = MathTex(r"\Delta_A = D_{KL}(\hat T' \parallel \bar T_A)", font_size=32)
+ gap = MathTex(r"g = \Delta_H - \Delta_A", font_size=36, color=HIGHLIGHT)
+
+ eqs = VGroup(t_prime, d_h, d_a, gap).arrange(DOWN, buff=0.2)
+ eqs.to_edge(LEFT, buff=1.0).shift(DOWN * 1.0)
+
+ self.play(Write(eqs))
+
+ # Distributions
+ axis = (
+ Axes(
+ x_range=[-8, 8, 2],
+ y_range=[0, 0.2, 0.05],
+ x_length=6,
+ y_length=3,
+ tips=False,
+ axis_config={"color": AXIS_INK, "stroke_width": 2},
+ )
+ .to_edge(RIGHT, buff=1.0)
+ .shift(DOWN * 1.0)
+ )
+
+ mu_h, sig_h = -3.5, 2.0
+ mu_a, sig_a = 3.5, 2.0
+
+ h_curve = axis.plot(
+ lambda x: normal_pdf(x, mu_h, sig_h), color=BLUE_D, stroke_width=4
+ )
+ a_curve = axis.plot(
+ lambda x: normal_pdf(x, mu_a, sig_a), color=RED_C, stroke_width=4
+ )
+
+ h_lbl = (
+ Text("Human", color=BLUE_D, font_size=20)
+ .next_to(h_curve, UP, buff=-0.5)
+ .shift(LEFT * 1)
+ )
+ a_lbl = (
+ Text("Agent", color=RED_C, font_size=20)
+ .next_to(a_curve, UP, buff=-0.5)
+ .shift(RIGHT * 1)
+ )
+
+ boundary = DashedLine(axis.c2p(0, 0), axis.c2p(0, 0.18), color=GREY_B)
+
+ self.play(FadeIn(axis))
+ self.play(Create(h_curve), Create(a_curve))
+ self.play(FadeIn(h_lbl), FadeIn(a_lbl), FadeIn(boundary))
+
+ sig_text = MathTex(
+ r"p<10^{-3}\ \text{(Mann--Whitney)}", font_size=24, color=GREEN_C
+ )
+ sig_text.next_to(axis, DOWN, buff=0.3)
+ self.play(Write(sig_text))
+ self.wait(1)
+
+
+class TrajectorySamplingScene(Scene):
+ def construct(self):
+ title = scene_title("Generative Trajectory Sampling")
+ self.play(Write(title))
+
+ agent_mat = to_matrix(
+ [
+ [0.00, 0.80, 0.20, 0.00, 0.00],
+ [0.00, 0.30, 0.50, 0.20, 0.00],
+ [0.00, 0.40, 0.30, 0.30, 0.00],
+ [0.00, 0.10, 0.10, 0.10, 0.70],
+ [0.00, 0.00, 0.00, 0.00, 1.00],
+ ],
+ "Agent Kernel T_A",
+ RED_C,
+ ).scale(0.6)
+ agent_mat.to_edge(LEFT, buff=1.0)
+
+ self.play(FadeIn(agent_mat))
+
+ states = ["Start", "View", "Detail", "Cart", "Buy"]
+
+ def create_node(text):
+ circ = Circle(radius=0.4, color=AXIS_INK, fill_opacity=0.1)
+ lbl = Text(text, font_size=16).move_to(circ)
+ return VGroup(circ, lbl)
+
+ nodes = VGroup(*[create_node(s) for s in states]).arrange(RIGHT, buff=0.6)
+ nodes.to_edge(RIGHT, buff=0.5).shift(UP * 1.0)
+
+ self.play(FadeIn(nodes))
+
+ # Output trajectory string
+ traj_label = (
+ Text("Sampled Trajectory:", font_size=24, color=HIGHLIGHT)
+ .to_edge(DOWN)
+ .shift(UP * 1.5 + LEFT * 1)
+ )
+ self.play(FadeIn(traj_label))
+
+ walker = Dot(color=HIGHLIGHT, radius=0.15)
+ walker.move_to(nodes[0].get_top() + UP * 0.2)
+
+ self.play(FadeIn(walker))
+
+ # Simulation
+ path = [0, 1, 2, 1, 2] # Start -> View -> Detail -> View -> Detail
+
+ # We will build the string
+ current_traj = VGroup(Text("Start", font_size=24, color=RED_C)).next_to(
+ traj_label, RIGHT
+ )
+ self.play(FadeIn(current_traj))
+
+ for i in range(len(path) - 1):
+ curr_state = path[i]
+ next_state = path[i + 1]
+
+ # highlight row
+ mat_core = agent_mat[2] # the matrix itself
+
+ # Using get_rows() which is standard in Mobject Matrix
+ row_entries = mat_core.get_rows()[curr_state]
+ row_rect = SurroundingRectangle(row_entries, color=HIGHLIGHT, buff=0.1)
+ self.play(Create(row_rect), run_time=0.5)
+
+ # move walker
+ arc = CurvedArrow(
+ walker.get_center(),
+ nodes[next_state].get_top() + UP * 0.2,
+ angle=-TAU / 4,
+ )
+ self.play(MoveAlongPath(walker, arc), run_time=1.0)
+
+ # Update string
+ arrow_str = MathTex(r"\rightarrow", font_size=24).next_to(
+ current_traj, RIGHT
+ )
+ next_str = Text(states[next_state], font_size=24, color=RED_C).next_to(
+ arrow_str, RIGHT
+ )
+
+ self.play(
+ FadeIn(arrow_str), FadeIn(next_str), FadeOut(row_rect), run_time=0.5
+ )
+ current_traj.add(arrow_str, next_str)
+
+ self.wait(1)
+
+
+class KroneckerExpansionScene(Scene):
+ def construct(self):
+ title = scene_title("State-Space Expansion")
+ self.play(Write(title))
+
+ t_mat = to_matrix([[0.2, 0.8], [0.4, 0.6]], "Behavior T", BLUE_D)
+
+ d_mat = to_matrix([[0.9, 0.1], [0.5, 0.5]], "Demand D", RED_C)
+
+ kron_sym = MathTex(r"\otimes", font_size=60)
+ eq_sym = MathTex(r"=", font_size=60)
+
+ lhs = VGroup(t_mat, kron_sym, d_mat).arrange(RIGHT, buff=0.5)
+ lhs.next_to(title, DOWN, buff=1.0)
+
+ self.play(FadeIn(t_mat), FadeIn(d_mat), Write(kron_sym))
+ self.wait(1)
+
+ self.play(lhs.animate.scale(0.6).to_edge(LEFT, buff=0.5))
+
+ # Show expanded
+ # T tensor D
+ expanded = to_matrix(
+ [
+ [0.18, 0.02, 0.72, 0.08],
+ [0.10, 0.10, 0.40, 0.40],
+ [0.36, 0.04, 0.54, 0.06],
+ [0.20, 0.20, 0.30, 0.30],
+ ],
+ r"Expanded P = T \otimes D",
+ HIGHLIGHT,
+ ).scale(0.6)
+
+ eq_sym.next_to(lhs, RIGHT, buff=0.5)
+ expanded.next_to(eq_sym, RIGHT, buff=0.5)
+
+ self.play(Write(eq_sym), FadeIn(expanded, shift=LEFT * 0.5))
+
+ # Highlight a block
+ # the top right block (0.8 * D)
+ # rows 0,1 cols 2,3
+ # In expanded:
+ # row 0: 0, 1, 2, 3
+ # row 1: 4, 5, 6, 7
+ t_entries = t_mat[2].get_entries()
+ if len(t_entries) >= 2:
+ rect_T = SurroundingRectangle(
+ t_entries[1], color=HIGHLIGHT
+ ) # T[0,1] is 0.8
+ else:
+ rect_T = VGroup()
+
+ exp_entries = expanded[2].get_entries()
+ if len(exp_entries) >= 8:
+ block_entries = VGroup(
+ exp_entries[2], exp_entries[3], exp_entries[6], exp_entries[7]
+ )
+ rect_block = SurroundingRectangle(block_entries, color=HIGHLIGHT)
+ else:
+ rect_block = VGroup()
+
+ desc = MathTex(
+ r"P(s', d' \mid s, d)=T(s'\mid s)\,D(d'\mid d, s')",
+ font_size=26,
+ color=HIGHLIGHT,
+ )
+ desc.next_to(expanded, DOWN, buff=0.5)
+
+ if len(t_entries) >= 2 and len(exp_entries) >= 8:
+ self.play(Create(rect_T), Create(rect_block))
+ self.play(Write(desc))
+ self.wait(1)
+
+
+class SamplingAndReservationScene(Scene):
+ def construct(self):
+ title = scene_title("Pricing Policy & Reservation Price")
+ self.play(Write(title))
+
+ # 1. The setup
+ setup = VGroup(
+ MathTex(r"p_i \sim \pi(p \mid \tau)", font_size=44),
+ MathTex(
+ r"\underline p = \text{reservation price}", font_size=38, color=ORANGE
+ ),
+ ).arrange(DOWN, aligned_edge=LEFT, buff=0.3)
+ setup.to_edge(LEFT, buff=1.0).shift(UP * 1.0)
+
+ self.play(Write(setup[0]))
+ self.play(Write(setup[1]))
+
+ # 2. Number line sampling
+ number_line = NumberLine(
+ x_range=[P_MIN, P_MAX, 10],
+ length=9.8,
+ color=AXIS_INK,
+ include_numbers=True,
+ decimal_number_config={"num_decimal_places": 0, "color": INK},
+ ).shift(DOWN * 1.0)
+
+ self.play(FadeIn(number_line))
+
+ # Floor marker
+ floor_marker = Line(
+ number_line.n2p(P_MIN),
+ number_line.n2p(P_MIN) + UP * 0.85,
+ color=ORANGE,
+ stroke_width=5,
+ )
+ floor_label = MathTex(r"\underline p", color=ORANGE).next_to(
+ floor_marker, UP, buff=0.05
+ )
+ self.play(Create(floor_marker), FadeIn(floor_label))
+
+ # Animate sampling
+ rng = np.random.default_rng(42)
+ n_samples = 5
+ draws = np.sort(rng.beta(2.5, 2.0, size=n_samples) * (P_MAX - P_MIN) + P_MIN)
+
+ dots = VGroup()
+ for i, val in enumerate(draws):
+ # Show drawing process
+ temp_dot = Dot(number_line.n2p(120), radius=0.08, color=BLUE_D).shift(
+ UP * 1.5
+ )
+ self.play(FadeIn(temp_dot), run_time=0.2)
+
+ final_pos = number_line.n2p(float(val))
+ self.play(temp_dot.animate.move_to(final_pos), run_time=0.3)
+ dots.add(temp_dot)
+
+ self.wait(0.5)
+
+ # Highlight minimum
+ min_dot = dots[0]
+ min_highlight = Circle(radius=0.15, color=RED_C).move_to(min_dot)
+ min_tag = MathTex(r"p_{(1)}", color=RED_C).next_to(min_highlight, UP, buff=0.1)
+
+ self.play(Create(min_highlight), Write(min_tag))
+
+ desc = MathTex(
+ r"\text{realized price }p_{(1)}=\min\{p_1,\ldots,p_N\}",
+ font_size=26,
+ color=GREY_B,
+ ).to_edge(DOWN)
+
+ self.play(FadeIn(desc, shift=UP * 0.2))
+ self.wait(1.5)
+
+
+class COIDistributionScene(Scene):
+ def construct(self):
+ title = scene_title("Cost of Information (COI)")
+ self.play(Write(title))
+
+ # COI definition
+ coi_def = MathTex(
+ r"\mathrm{COI} = \mathbb{E}[P] - \underline p",
+ font_size=46,
+ color=HIGHLIGHT,
+ ).next_to(title, DOWN, buff=0.5)
+
+ self.play(Write(coi_def))
+
+ # Distribution plot
+ floor_x = 86.0
+ mean_x = 116.0
+ axes = Axes(
+ x_range=[80, 160, 10],
+ y_range=[0.0, 0.04, 0.01],
+ x_length=8.0,
+ y_length=4.0,
+ tips=False,
+ axis_config={"stroke_width": 2, "color": AXIS_INK},
+ ).shift(DOWN * 0.5)
+
+ density = axes.plot(
+ lambda x: normal_pdf(x, mean_x, 12.0),
+ x_range=[80, 160],
+ color=BLUE_D,
+ stroke_width=6,
+ )
+
+ area = axes.get_area(density, x_range=[80, 160], color=BLUE_D, opacity=0.2)
+
+ self.play(FadeIn(axes))
+ self.play(Create(density), FadeIn(area))
+
+ # Markers
+ floor_line = DashedLine(
+ axes.c2p(floor_x, 0.0),
+ axes.c2p(floor_x, 0.038),
+ color=ORANGE,
+ stroke_width=4,
+ )
+ mean_line = DashedLine(
+ axes.c2p(mean_x, 0.0),
+ axes.c2p(mean_x, 0.038),
+ color=GREEN_C,
+ stroke_width=4,
+ )
+
+ floor_tag = MathTex(r"\underline p", color=ORANGE).next_to(
+ floor_line, UP, buff=0.1
+ )
+ mean_tag = MathTex(r"\mathbb{E}[P]", color=GREEN_C).next_to(
+ mean_line, UP, buff=0.1
+ )
+
+ self.play(Create(floor_line), Write(floor_tag))
+ self.play(Create(mean_line), Write(mean_tag))
+
+ # COI span
+ coi_arrow = DoubleArrow(
+ axes.c2p(floor_x, 0.02), axes.c2p(mean_x, 0.02), color=HIGHLIGHT, buff=0
+ )
+ coi_label = Text("COI", font_size=24, color=HIGHLIGHT).next_to(
+ coi_arrow, UP, buff=0.1
+ )
+
+ self.play(GrowFromCenter(coi_arrow), Write(coi_label))
+
+ desc = MathTex(
+ r"\mathrm{COI}=\mathbb{E}[P]-\underline p",
+ font_size=28,
+ color=GREY_B,
+ ).to_edge(DOWN)
+
+ self.play(FadeIn(desc, shift=UP * 0.2))
+ self.wait(1.5)
+
+
+class COIErosionMathScene(Scene):
+ def construct(self):
+ title = scene_title("Mathematical Proof of COI Erosion")
+ self.play(Write(title))
+
+ # Step 1: Expected value of minimum
+ eq1 = MathTex(
+ r"\mathbb{E}[p_{(1)}] = \underline p + \int_{\underline p}^{\bar p} \mathbb{P}(p_{(1)} > t) dt",
+ font_size=36,
+ )
+
+ # Step 2: Probability of minimum > t
+ eq2 = MathTex(
+ r"\mathbb{P}(p_{(1)} > t) = \mathbb{P}(p_1 > t) \times \dots \times \mathbb{P}(p_N > t)",
+ font_size=36,
+ )
+
+ # Step 3: Assuming i.i.d
+ eq3 = MathTex(r"= [1 - F_\pi(t)]^N", font_size=36, color=HIGHLIGHT)
+
+ # Step 4: Substitute back
+ eq4 = MathTex(
+ r"\mathbb{E}[p_{(1)}] = \underline p + \int_{\underline p}^{\bar p} [1 - F_\pi(t)]^N dt",
+ font_size=36,
+ )
+
+ # Step 5: Limit as N -> inf
+ eq5_pt1 = MathTex(
+ r"\text{Since } [1 - F_\pi(t)] < 1 \text{ for } t > \underline p:",
+ font_size=32,
+ color=GREY_B,
+ )
+
+ eq5_pt2 = MathTex(
+ r"\lim_{N \to \infty} \mathbb{E}[p_{(1)}] = \underline p",
+ font_size=42,
+ color=RED_C,
+ )
+
+ eq6 = MathTex(
+ r"\lim_{N \to \infty} \mathrm{COI} = 0", font_size=46, color=HIGHLIGHT
+ )
+
+ group = VGroup(eq1, eq2, eq3, eq4, eq5_pt1, eq5_pt2, eq6).arrange(
+ DOWN, aligned_edge=LEFT, buff=0.4
+ )
+ group.next_to(title, DOWN, buff=0.5).shift(RIGHT * 1.5)
+
+ # We want eq3 to be right after eq2
+ eq3.next_to(eq2, RIGHT, buff=0.2)
+
+ # Re-arrange carefully
+ step1 = eq1.copy().to_edge(LEFT, buff=1.0).shift(UP * 1.5)
+ step2 = (
+ VGroup(eq2.copy(), eq3.copy())
+ .arrange(RIGHT, buff=0.2)
+ .next_to(step1, DOWN, aligned_edge=LEFT, buff=0.5)
+ )
+ step3 = eq4.copy().next_to(step2, DOWN, aligned_edge=LEFT, buff=0.5)
+
+ step4_group = (
+ VGroup(eq5_pt1.copy(), eq5_pt2.copy())
+ .arrange(DOWN, aligned_edge=LEFT, buff=0.2)
+ .next_to(step3, DOWN, aligned_edge=LEFT, buff=0.5)
+ )
+
+ step5 = eq6.copy().next_to(step4_group, DOWN, buff=0.6).match_x(title)
+
+ # Animate
+ self.play(Write(step1))
+ self.wait(0.5)
+
+ self.play(Write(step2[0]))
+ self.play(Write(step2[1]))
+ self.wait(0.5)
+
+ self.play(Write(step3))
+ self.wait(0.5)
+
+ self.play(Write(step4_group[0]))
+ self.play(Write(step4_group[1]))
+ self.wait(0.5)
+
+ # Put a box around the final conclusion
+ box = SurroundingRectangle(step5, color=HIGHLIGHT, buff=0.2)
+ self.play(Write(step5), Create(box))
+
+ desc = MathTex(
+ r"N\to\infty\ \Rightarrow\ \mathrm{COI}\to 0",
+ font_size=28,
+ color=GREY_B,
+ ).to_edge(DOWN)
+
+ self.play(FadeIn(desc, shift=UP * 0.2))
+ self.wait(2)
+
+BEHAVIOR_SCENES = [
+ "LogsToKernelsScene",
+ "KLSeparabilityAndSignificanceScene",
+ "TrajectorySamplingScene",
+ "KroneckerExpansionScene",
+]
+
+COI_SCENES = [
+ "SamplingAndReservationScene",
+ "COIDistributionScene",
+ "COIErosionMathScene",
+]
diff --git a/paper/defense/manim/scenes/main.py b/paper/defense/manim/scenes/main.py
new file mode 100644
index 0000000..eebf572
--- /dev/null
+++ b/paper/defense/manim/scenes/main.py
@@ -0,0 +1,1523 @@
+from __future__ import annotations
+
+import numpy as np
+from manim import (
+ Axes,
+ Arrow,
+ BarChart,
+ BLUE_D,
+ Circle,
+ Circumscribe,
+ Create,
+ CurvedArrow,
+ DashedLine,
+ DecimalNumber,
+ Dot,
+ DOWN,
+ FadeIn,
+ FadeOut,
+ GREEN_C,
+ GREY_B,
+ Indicate,
+ LaggedStart,
+ LEFT,
+ Line,
+ MathTex,
+ NumberLine,
+ ORANGE,
+ Rectangle,
+ RED_C,
+ RIGHT,
+ Scene,
+ SurroundingRectangle,
+ Text,
+ Transform,
+ UP,
+ ValueTracker,
+ VGroup,
+ Write,
+ always_redraw,
+ smooth,
+)
+from common import (
+ AXIS_INK,
+ HIGHLIGHT,
+ INK,
+ P_MAX,
+ P_MIN,
+ actor_face_card,
+ card,
+ normal_pdf,
+ private_valuation_card,
+ product_suit_card,
+ rank_from_scale,
+ scene_title,
+ to_matrix,
+)
+
+
+class DefenseOpening(Scene):
+ def construct(self) -> None:
+ title = scene_title("PHANTOM")
+ tag = MathTex(
+ r"\text{dynamic pricing under agent-mediated traffic}",
+ font_size=30,
+ color=GREY_B,
+ ).next_to(title, DOWN, buff=0.2)
+ self.play(Write(title), FadeIn(tag, shift=UP * 0.12))
+
+ dist_axes = Axes(
+ x_range=[-6, 6, 2],
+ y_range=[0.0, 0.2, 0.05],
+ x_length=3.1,
+ y_length=1.75,
+ tips=False,
+ axis_config={"stroke_width": 1.8, "color": AXIS_INK},
+ )
+ dist_h = dist_axes.plot(
+ lambda x: normal_pdf(x, -1.9, 1.6),
+ x_range=[-6, 6],
+ color=BLUE_D,
+ stroke_width=4,
+ )
+ dist_a = dist_axes.plot(
+ lambda x: normal_pdf(x, 1.8, 1.8),
+ x_range=[-6, 6],
+ color=RED_C,
+ stroke_width=4,
+ )
+ dist_block = VGroup(
+ dist_axes,
+ dist_h,
+ dist_a,
+ MathTex(r"g=\Delta_H-\Delta_A", font_size=22, color=GREY_B).next_to(
+ dist_axes, DOWN, buff=0.05
+ ),
+ )
+
+ tail_axes = Axes(
+ x_range=[0, 1, 0.2],
+ y_range=[0, 1, 0.2],
+ x_length=3.1,
+ y_length=1.75,
+ tips=False,
+ axis_config={"stroke_width": 1.8, "color": AXIS_INK},
+ )
+ tail_n1 = tail_axes.plot(
+ lambda x: (1 - x) ** 1,
+ x_range=[0, 1],
+ color=GREEN_C,
+ stroke_width=4,
+ )
+ tail_n8 = tail_axes.plot(
+ lambda x: (1 - x) ** 8,
+ x_range=[0, 1],
+ color=HIGHLIGHT,
+ stroke_width=4,
+ )
+ tail_block = VGroup(
+ tail_axes,
+ tail_n1,
+ tail_n8,
+ MathTex(r"[1{-}F(t)]^N", font_size=22, color=GREY_B).next_to(
+ tail_axes, DOWN, buff=0.05
+ ),
+ )
+
+ control_eq = MathTex(
+ r"\hat\alpha(\tau')\Rightarrow\pi^*",
+ font_size=36,
+ color=HIGHLIGHT,
+ )
+ control_box = SurroundingRectangle(control_eq, color=HIGHLIGHT, buff=0.12)
+ control_block = VGroup(control_box, control_eq)
+
+ preview = VGroup(dist_block, tail_block, control_block).arrange(
+ RIGHT, buff=0.5
+ )
+ preview.next_to(tag, DOWN, buff=0.48)
+ preview_caption = MathTex(
+ r"\text{separability}\ \to\ \text{tail}\ \to\ \text{policy}",
+ font_size=24,
+ color=GREY_B,
+ ).next_to(preview, UP, buff=0.1)
+
+ f_arrow_1 = Arrow(dist_block.get_right(), tail_block.get_left(), buff=0.08)
+ f_arrow_2 = Arrow(tail_block.get_right(), control_block.get_left(), buff=0.08)
+
+ self.play(FadeIn(preview_caption, shift=UP * 0.1))
+ self.play(FadeIn(dist_block), FadeIn(tail_block), FadeIn(control_block))
+ self.play(FadeIn(f_arrow_1), FadeIn(f_arrow_2))
+ self.play(Indicate(control_block, color=HIGHLIGHT, run_time=1.0))
+ self.wait(0.75)
+
+
+class CardMarketAnalogyScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Card Analogy: Platform, Customer, Agent")
+ self.play(Write(title))
+
+ subtitle = MathTex(
+ r"K\text{ (platform)},\ Q\text{ (customer)},\ J\text{ (agent)},\ \clubsuit\text{--}\diamondsuit=\text{SKUs}",
+ font_size=22,
+ color=GREY_B,
+ ).next_to(title, DOWN, buff=0.14)
+ self.play(FadeIn(subtitle, shift=UP * 0.05))
+
+ king = actor_face_card(
+ rank="K", role="platform", accent=ORANGE, show_role=False
+ )
+ king.move_to(LEFT * 5.35 + DOWN * 0.35)
+
+ queen_home = RIGHT * 3.2 + DOWN * 0.28
+ queen = actor_face_card(
+ rank="Q", role="customer", accent=BLUE_D, show_role=False
+ )
+ queen.move_to(queen_home)
+
+ valuation = private_valuation_card(value=5).next_to(queen, RIGHT, buff=0.35)
+
+ specs = [
+ ("C", INK, 4),
+ ("H", RED_C, 6),
+ ("S", INK, 5),
+ ("D", RED_C, 3),
+ ]
+ scales = [initial for _, _, initial in specs]
+ products = VGroup()
+ scale_tokens: list[Text] = []
+ for suit, color, initial in specs:
+ product_card, token = product_suit_card(
+ suit=suit, scale=initial, accent=color
+ )
+ products.add(product_card)
+ scale_tokens.append(token)
+
+ products.arrange(DOWN, buff=0.15).move_to(LEFT * 1.75 + DOWN * 0.55)
+
+ actor_link = Arrow(
+ king.get_right(),
+ products.get_left(),
+ buff=0.15,
+ color=HIGHLIGHT,
+ stroke_width=3.6,
+ )
+
+ self.play(
+ FadeIn(king, shift=RIGHT * 0.2),
+ FadeIn(products, shift=UP * 0.15),
+ FadeIn(queen, shift=LEFT * 0.2),
+ FadeIn(valuation, shift=LEFT * 0.2),
+ )
+ self.play(FadeIn(actor_link))
+
+ stage = Text(
+ "Stage 1: queen browses directly and visited products rise in scale.",
+ font_size=21,
+ color=GREY_B,
+ ).to_edge(DOWN)
+ self.play(FadeIn(stage, shift=UP * 0.08))
+
+ direct_visits = [1, 2]
+ for idx in direct_visits:
+ target = products[idx]
+ demand_box = SurroundingRectangle(target, color=BLUE_D, buff=0.06)
+ king_box = SurroundingRectangle(king[0], color=HIGHLIGHT, buff=0.07)
+
+ self.play(
+ queen.animate.move_to(target.get_right() + RIGHT * 0.9),
+ run_time=0.7,
+ )
+ self.play(Create(demand_box), run_time=0.2)
+
+ scales[idx] = min(10, scales[idx] + 2)
+ new_scale = Text(
+ rank_from_scale(scales[idx]),
+ font_size=40,
+ weight="BOLD",
+ color=specs[idx][1],
+ ).move_to(scale_tokens[idx])
+ self.play(
+ Create(king_box),
+ Transform(scale_tokens[idx], new_scale),
+ run_time=0.5,
+ )
+ self.play(FadeOut(king_box), FadeOut(demand_box), run_time=0.18)
+
+ self.play(queen.animate.move_to(queen_home), run_time=0.7)
+
+ stage_two = Text(
+ "Stage 2: queen hires jack to search every card before deciding.",
+ font_size=21,
+ color=GREY_B,
+ ).to_edge(DOWN)
+ self.play(Transform(stage, stage_two))
+
+ jack = actor_face_card(
+ rank="J", role="agent", accent=RED_C, show_role=False
+ ).scale(0.95)
+ jack.next_to(queen, LEFT, buff=0.35)
+ hire_arrow = Arrow(
+ queen.get_left(),
+ jack.get_right(),
+ buff=0.08,
+ color=HIGHLIGHT,
+ stroke_width=2.6,
+ )
+ self.play(FadeIn(jack, shift=RIGHT * 0.16), FadeIn(hire_arrow))
+ self.play(FadeOut(hire_arrow), run_time=0.2)
+
+ for idx, target in enumerate(products):
+ demand_box = SurroundingRectangle(target, color=RED_C, buff=0.05)
+ king_box = SurroundingRectangle(king[0], color=HIGHLIGHT, buff=0.07)
+
+ self.play(
+ jack.animate.move_to(target.get_right() + RIGHT * 0.62),
+ run_time=0.32,
+ )
+ self.play(Create(demand_box), run_time=0.17)
+
+ scales[idx] = min(10, scales[idx] + 1)
+ new_scale = Text(
+ rank_from_scale(scales[idx]),
+ font_size=40,
+ weight="BOLD",
+ color=specs[idx][1],
+ ).move_to(scale_tokens[idx])
+ self.play(
+ Create(king_box), Transform(scale_tokens[idx], new_scale), run_time=0.38
+ )
+ self.play(
+ FadeOut(king_box),
+ FadeOut(demand_box),
+ run_time=0.15,
+ )
+
+ self.play(jack.animate.next_to(queen, LEFT, buff=0.35), run_time=0.55)
+
+ report_arrow = Arrow(
+ jack.get_right(),
+ queen.get_left(),
+ buff=0.08,
+ color=GREEN_C,
+ stroke_width=2.6,
+ )
+ self.play(FadeIn(report_arrow))
+
+ best_idx = int(np.argmin(scales))
+ best_card = products[best_idx]
+ choice_box = SurroundingRectangle(best_card, color=GREEN_C, buff=0.07)
+ stage_three = Text(
+ "Decision rule: buy when private value v* exceeds shown scale.",
+ font_size=21,
+ color=GREY_B,
+ ).to_edge(DOWN)
+
+ self.play(
+ Transform(stage, stage_three),
+ queen.animate.move_to(best_card.get_right() + RIGHT * 0.9),
+ Create(choice_box),
+ run_time=0.95,
+ )
+ self.play(
+ FadeOut(jack),
+ FadeOut(report_arrow),
+ FadeOut(actor_link),
+ FadeOut(subtitle),
+ )
+ self.wait(1.0)
+
+
+class COIFirstPrinciplesScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Cost of Information from First Principles")
+ self.play(Write(title))
+
+ setup = VGroup(
+ MathTex(r"P\sim\pi(\tau)", font_size=44),
+ MathTex(r"\underline p=\text{reservation price}", font_size=38),
+ MathTex(r"M=P-\underline p", font_size=46, color=HIGHLIGHT),
+ ).arrange(DOWN, aligned_edge=LEFT, buff=0.22)
+ setup.to_edge(LEFT).shift(UP * 0.55)
+
+ self.play(
+ LaggedStart(
+ *[FadeIn(line, shift=RIGHT * 0.2) for line in setup], lag_ratio=0.2
+ )
+ )
+
+ floor_x = 86.0
+ mean_x = 116.0
+ axes = (
+ Axes(
+ x_range=[80, 160, 10],
+ y_range=[0.0, 0.04, 0.01],
+ x_length=7.0,
+ y_length=3.3,
+ tips=False,
+ axis_config={"stroke_width": 2, "color": AXIS_INK},
+ )
+ .to_edge(RIGHT)
+ .shift(DOWN * 0.2)
+ )
+ density = axes.plot(
+ lambda x: normal_pdf(x, mean_x, 12.0),
+ x_range=[80, 160],
+ color=BLUE_D,
+ stroke_width=6,
+ )
+ floor_line = Line(
+ axes.c2p(floor_x, 0.0),
+ axes.c2p(floor_x, 0.036),
+ color=ORANGE,
+ stroke_width=4,
+ )
+ mean_line = Line(
+ axes.c2p(mean_x, 0.0),
+ axes.c2p(mean_x, 0.036),
+ color=GREEN_C,
+ stroke_width=4,
+ )
+ floor_tag = (
+ MathTex(r"\underline p", color=ORANGE)
+ .scale(0.72)
+ .next_to(floor_line, UP, buff=0.06)
+ )
+ mean_tag = (
+ MathTex(r"\mathbb{E}[P]", color=GREEN_C)
+ .scale(0.72)
+ .next_to(mean_line, UP, buff=0.06)
+ )
+ coi_span = Line(
+ axes.c2p(floor_x, 0.032),
+ axes.c2p(mean_x, 0.032),
+ color=HIGHLIGHT,
+ stroke_width=6,
+ )
+ coi_tag = MathTex(
+ r"\mathbb{E}[P]-\underline p", font_size=22, color=HIGHLIGHT
+ ).next_to(coi_span, UP, buff=0.05)
+
+ chart = VGroup(
+ axes,
+ density,
+ floor_line,
+ mean_line,
+ floor_tag,
+ mean_tag,
+ coi_span,
+ coi_tag,
+ )
+
+ self.play(FadeIn(axes), FadeIn(density))
+ self.play(
+ FadeIn(floor_line), FadeIn(mean_line), FadeIn(floor_tag), FadeIn(mean_tag)
+ )
+ self.play(FadeIn(coi_span), FadeIn(coi_tag))
+ self.play(
+ FadeOut(setup, shift=LEFT * 0.15),
+ chart.animate.scale(0.82).to_edge(RIGHT).shift(UP * 0.6),
+ )
+
+ coi_left = MathTex(r"\mathrm{COI}=\mathbb{E}[", font_size=42)
+ coi_mid = MathTex(r"M", font_size=42)
+ coi_right = MathTex(r"]", font_size=42)
+ coi_eq = VGroup(coi_left, coi_mid, coi_right).arrange(RIGHT, buff=0.04)
+ coi_eq.to_edge(LEFT).shift(UP * 0.45)
+
+ self.play(Write(coi_left), FadeIn(coi_mid, shift=UP * 0.05), Write(coi_right))
+
+ expanded_mid = MathTex(r"P-\underline p", font_size=42)
+ expanded_mid.move_to(coi_mid, aligned_edge=LEFT)
+ self.play(
+ Transform(coi_mid, expanded_mid),
+ coi_right.animate.next_to(coi_mid, RIGHT, buff=0.04),
+ )
+ self.play(coi_eq.animate.set_color(HIGHLIGHT))
+ self.play(Indicate(coi_eq, color=HIGHLIGHT, run_time=0.85))
+
+ survival = MathTex(
+ r"\mathrm{COI}=\int_{\underline p}^{\bar p}(1-F_\pi(p))\,dp",
+ font_size=33,
+ color=GREY_B,
+ ).next_to(coi_eq, DOWN, aligned_edge=LEFT, buff=0.2)
+ self.play(Write(survival))
+
+ identity_1 = MathTex(
+ r"\mathbb E[X]=\int_0^{\infty}\mathbb P(X>u)\,du\quad (X\ge 0)",
+ font_size=31,
+ color=GREY_B,
+ ).next_to(survival, DOWN, aligned_edge=LEFT, buff=0.2)
+ identity_2 = MathTex(
+ r"X=P-\underline p,\;u=p-\underline p\Rightarrow\int_{\underline p}^{\bar p}(1-F_\pi(p))\,dp",
+ font_size=31,
+ color=GREY_B,
+ ).next_to(identity_1, DOWN, aligned_edge=LEFT, buff=0.14)
+ self.play(Write(identity_1))
+ self.play(Write(identity_2))
+ self.wait(1.0)
+
+
+class COIOrderStatisticProofScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Why COI Erodes with Agent Saturation")
+ self.play(Write(title))
+
+ scope = MathTex(
+ r"\text{Assumption: independent sessions; no shared quotes across agents}",
+ font_size=22,
+ color=GREY_B,
+ ).next_to(title, DOWN, buff=0.16)
+ self.play(FadeIn(scope, shift=DOWN * 0.06))
+
+ key = MathTex(r"p_{(1)}=\min(p_1,\ldots,p_N)", font_size=42, color=HIGHLIGHT)
+ key.next_to(scope, DOWN, buff=0.22)
+ self.play(Write(key))
+ self.play(Circumscribe(key, color=HIGHLIGHT, run_time=0.85))
+
+ number_line = NumberLine(
+ x_range=[P_MIN, P_MAX, 10],
+ length=9.8,
+ color=AXIS_INK,
+ include_numbers=True,
+ decimal_number_config={"num_decimal_places": 0, "color": INK},
+ ).shift(DOWN * 1.5)
+ floor_marker = Line(
+ number_line.n2p(P_MIN),
+ number_line.n2p(P_MIN) + UP * 0.85,
+ color=ORANGE,
+ stroke_width=5,
+ )
+ floor_label = MathTex(r"\underline p", color=ORANGE).next_to(
+ floor_marker, UP, buff=0.05
+ )
+ self.play(FadeIn(number_line), FadeIn(floor_marker), FadeIn(floor_label))
+
+ rng = np.random.default_rng(17)
+ current_group: VGroup | None = None
+ current_info: VGroup | None = None
+
+ for n in [1, 3, 8, 20]:
+ draws = np.sort(rng.beta(2.4, 2.1, size=n) * (P_MAX - P_MIN) + P_MIN)
+ dots = VGroup(
+ *[
+ Dot(number_line.n2p(float(v)), radius=0.06, color=BLUE_D)
+ for v in draws
+ ]
+ )
+ min_dot = Dot(number_line.n2p(float(draws[0])), radius=0.09, color=RED_C)
+ min_tag = (
+ MathTex(r"p_{(1)}", color=RED_C)
+ .scale(0.65)
+ .next_to(min_dot, UP, buff=0.08)
+ )
+ step_group = VGroup(dots, min_dot, min_tag)
+
+ info = VGroup(
+ Text(f"N = {n}", font_size=28),
+ Text(f"min observed = {draws[0]:.2f}", font_size=24),
+ ).arrange(DOWN, aligned_edge=LEFT, buff=0.12)
+ info.to_edge(LEFT).shift(UP * 0.55)
+ info_box = VGroup(SurroundingRectangle(info, color=GREY_B, buff=0.18), info)
+
+ if current_group is None:
+ self.play(FadeIn(step_group), FadeIn(info_box))
+ else:
+ self.play(
+ FadeOut(current_group),
+ FadeOut(current_info),
+ FadeIn(step_group),
+ FadeIn(info_box),
+ )
+ current_group = step_group
+ current_info = info_box
+ self.wait(0.4)
+
+ p1 = MathTex(
+ r"\mathbb{P}(p_{(1)}>t)=\mathbb{P}(p_1>t,\ldots,p_N>t)", font_size=36
+ )
+ p2 = MathTex(r"\mathbb{P}(p_{(1)}>t)=[1-F(t)]^N", font_size=42, color=HIGHLIGHT)
+ prob_group = VGroup(p1, p2).arrange(DOWN, aligned_edge=LEFT, buff=0.16)
+ prob_group.to_edge(RIGHT).shift(UP * 0.75)
+
+ self.play(Write(p1))
+ self.play(Write(p2))
+
+ cleanup_items: list = [key, number_line, floor_marker, floor_label]
+ if current_group is not None:
+ cleanup_items.append(current_group)
+ if current_info is not None:
+ cleanup_items.append(current_info)
+ self.play(
+ FadeOut(VGroup(*cleanup_items), shift=DOWN * 0.12),
+ prob_group.animate.shift(UP * 0.26),
+ )
+
+ tail_axes = (
+ Axes(
+ x_range=[0, 1, 0.2],
+ y_range=[0, 1, 0.2],
+ x_length=4.1,
+ y_length=2.45,
+ tips=False,
+ axis_config={"stroke_width": 2, "color": AXIS_INK},
+ )
+ .to_edge(RIGHT)
+ .shift(DOWN * 1.0 + LEFT * 0.2)
+ )
+ curve_1 = tail_axes.plot(
+ lambda x: (1 - x) ** 1, x_range=[0, 1], color=BLUE_D, stroke_width=4
+ )
+ curve_4 = tail_axes.plot(
+ lambda x: (1 - x) ** 4, x_range=[0, 1], color=GREEN_C, stroke_width=4
+ )
+ curve_16 = tail_axes.plot(
+ lambda x: (1 - x) ** 16, x_range=[0, 1], color=RED_C, stroke_width=4
+ )
+ c_labels = VGroup(
+ Text("N=1", font_size=18, color=BLUE_D),
+ Text("N=4", font_size=18, color=GREEN_C),
+ Text("N=16", font_size=18, color=RED_C),
+ ).arrange(DOWN, aligned_edge=LEFT, buff=0.08)
+ c_labels.next_to(tail_axes, UP, buff=0.08).align_to(tail_axes, RIGHT)
+ tail_x = MathTex(r"F(t)", font_size=24).next_to(tail_axes, DOWN, buff=0.05)
+ tail_y = MathTex(r"[1-F(t)]^N", font_size=24).next_to(
+ tail_axes, LEFT, buff=0.05
+ )
+
+ self.play(FadeIn(tail_axes), Create(curve_1), Create(curve_4), Create(curve_16))
+ self.play(FadeIn(c_labels), FadeIn(tail_x), FadeIn(tail_y))
+
+ e1 = MathTex(
+ r"\mathbb{E}[p_{(1)}]=\underline p+\int_{\underline p}^{\bar p}[1-F(t)]^N\,dt",
+ font_size=32,
+ )
+ e2 = MathTex(
+ r"X=p_{(1)}-\underline p\ge 0,\quad \mathbb E[X]=\int_0^{\infty}\mathbb P(X>u)\,du",
+ font_size=27,
+ color=GREY_B,
+ )
+ e3 = MathTex(
+ r"\mathbb P(X>u)=\mathbb P\!\left(p_{(1)}>\underline p+u\right)=[1-F(\underline p+u)]^N",
+ font_size=27,
+ color=GREY_B,
+ )
+ e4 = MathTex(
+ r"0\le[1-F(t)]^N\le1,\quad [1-F(t)]^N\to0\ \text{for } t>\underline p",
+ font_size=27,
+ color=GREY_B,
+ )
+ e5 = MathTex(
+ r"\Rightarrow\ \lim_{N\to\infty}(\mathbb{E}[p_{(1)}]-\underline p)=0",
+ font_size=38,
+ color=HIGHLIGHT,
+ )
+ proof_block = VGroup(e1, e2, e3, e4, e5).arrange(
+ DOWN, aligned_edge=LEFT, buff=0.12
+ )
+ proof_block.to_edge(LEFT).shift(UP * 0.45)
+ self.play(Write(e1))
+ self.play(Write(e2))
+ self.play(Write(e3))
+ self.play(Write(e4))
+ self.play(Write(e5))
+
+ conclusion = MathTex(
+ r"N\to\infty\ \Rightarrow\ \mathbb{E}[p_{(1)}]-\underline p\to 0",
+ font_size=28,
+ color=GREY_B,
+ )
+ conclusion.to_edge(DOWN)
+ self.play(FadeIn(conclusion, shift=UP * 0.1))
+ self.play(Indicate(conclusion, color=HIGHLIGHT, run_time=0.9))
+ self.wait(0.85)
+
+
+class BehaviorKernelConstructionScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("From Session Paths to Transition Kernels")
+ self.play(Write(title))
+
+ traj_h = MathTex(
+ r"\tau_H:\ s_0\!\to s_1\!\to s_2\!\to s_3\!\to s_T",
+ font_size=28,
+ color=GREEN_C,
+ )
+ traj_a = MathTex(
+ r"\tau_A:\ s_0\!\to s_1\!\to s_2\!\to s_1\!\to s_2\!\to\cdots",
+ font_size=28,
+ color=RED_C,
+ )
+ trajectories = VGroup(traj_h, traj_a).arrange(
+ DOWN, aligned_edge=LEFT, buff=0.16
+ )
+ trajectories.next_to(title, DOWN, buff=0.45).align_to(title, LEFT)
+ self.play(
+ LaggedStart(
+ *[FadeIn(t, shift=RIGHT * 0.2) for t in trajectories], lag_ratio=0.25
+ )
+ )
+
+ mle = MathTex(
+ r"\hat P(s'\mid s)=\frac{N(s,s')}{\sum_k N(s,k)}",
+ font_size=40,
+ color=HIGHLIGHT,
+ )
+ mle.next_to(trajectories, DOWN, aligned_edge=LEFT, buff=0.28)
+ self.play(Write(mle))
+
+ counts = to_matrix(
+ (
+ (0.00, 8.00, 0.00, 0.00),
+ (0.00, 2.00, 5.00, 1.00),
+ (0.00, 3.00, 2.00, 4.00),
+ (0.00, 1.00, 0.00, 6.00),
+ ),
+ "transition counts N(s,s')",
+ color=BLUE_D,
+ )
+ probs = to_matrix(
+ (
+ (0.00, 1.00, 0.00, 0.00),
+ (0.00, 0.25, 0.62, 0.13),
+ (0.00, 0.33, 0.22, 0.45),
+ (0.00, 0.14, 0.00, 0.86),
+ ),
+ "normalized kernel T",
+ color=GREEN_C,
+ header_buff=0.4,
+ )
+ mats = (
+ VGroup(counts, probs)
+ .arrange(RIGHT, buff=0.95)
+ .scale(0.92)
+ .to_edge(DOWN)
+ .shift(UP * 0.34)
+ )
+ arrow = Arrow(counts.get_right(), probs.get_left(), buff=0.18, stroke_width=4)
+ arrow_tag = MathTex(
+ r"\text{row-normalize}", font_size=20, color=GREY_B
+ ).next_to(arrow, UP, buff=0.08)
+ kernel_arrow = Arrow(
+ mle.get_bottom(),
+ mats.get_top() + UP * 0.05,
+ buff=0.1,
+ color=GREY_B,
+ stroke_width=3.2,
+ )
+ self.play(
+ FadeIn(mats, shift=UP * 0.12),
+ FadeIn(arrow),
+ FadeIn(arrow_tag),
+ FadeIn(kernel_arrow, shift=DOWN * 0.06),
+ )
+ self.play(
+ FadeOut(mle, shift=UP * 0.08),
+ FadeOut(kernel_arrow, shift=DOWN * 0.08),
+ )
+
+ note = MathTex(
+ r"\bar T_H,\bar T_A\ \text{feed KL and }\hat\alpha(\tau')",
+ font_size=22,
+ color=GREY_B,
+ )
+ note.next_to(mats, DOWN, buff=0.16)
+ self.play(FadeIn(note, shift=UP * 0.1))
+ self.wait(1.0)
+
+
+class SeparabilitySignalScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Separability as Control Signal")
+ self.play(Write(title))
+
+ human = to_matrix(
+ (
+ (0.05, 0.70, 0.20, 0.05),
+ (0.05, 0.20, 0.60, 0.15),
+ (0.10, 0.25, 0.30, 0.35),
+ (0.00, 0.00, 0.00, 1.00),
+ ),
+ "human centroid T_H",
+ color=GREEN_C,
+ )
+ agent = to_matrix(
+ (
+ (0.03, 0.82, 0.12, 0.03),
+ (0.06, 0.55, 0.21, 0.18),
+ (0.08, 0.48, 0.14, 0.30),
+ (0.00, 0.00, 0.00, 1.00),
+ ),
+ "agent centroid T_A",
+ color=RED_C,
+ )
+ kernels = VGroup(human, agent).arrange(RIGHT, buff=0.95).shift(UP * 0.45)
+ self.play(FadeIn(kernels, shift=UP * 0.15))
+
+ self.play(
+ kernels.animate.scale(0.6)
+ .arrange(DOWN, aligned_edge=LEFT, buff=0.24)
+ .to_edge(LEFT)
+ .shift(UP * 0.18)
+ )
+
+ d_h = MathTex(r"\Delta_H=D_{KL}(\hat T'\parallel\bar T_H)", font_size=36)
+ d_a = MathTex(r"\Delta_A=D_{KL}(\hat T'\parallel\bar T_A)", font_size=36)
+ gap = MathTex(r"g=\Delta_H-\Delta_A", font_size=44, color=HIGHLIGHT)
+ alpha = MathTex(r"\hat\alpha(\tau')=\sigma(\beta g)", font_size=40)
+ eqs = VGroup(d_h, d_a, gap, alpha).arrange(DOWN, aligned_edge=LEFT, buff=0.2)
+ eqs.to_edge(RIGHT).shift(UP * 0.38)
+ self.play(LaggedStart(*[Write(eq) for eq in eqs], lag_ratio=0.18))
+ self.play(Indicate(gap, color=HIGHLIGHT, run_time=0.85))
+
+ self.play(
+ eqs.animate.scale(0.66).next_to(kernels, DOWN, aligned_edge=LEFT, buff=0.16)
+ )
+
+ mu_h, sigma_h = -3.35, 2.67
+ mu_a, sigma_a = 1.65, 2.83
+ axis = (
+ Axes(
+ x_range=[-10, 10, 2],
+ y_range=[0.0, 0.18, 0.03],
+ x_length=6.8,
+ y_length=3.7,
+ tips=False,
+ axis_config={"stroke_width": 2, "color": AXIS_INK},
+ )
+ .to_edge(RIGHT)
+ .shift(DOWN * 0.75 + LEFT * 0.15)
+ )
+ x_tag = MathTex(r"g=\Delta_H-\Delta_A", font_size=30).next_to(
+ axis, DOWN, buff=0.15
+ )
+
+ human_curve = axis.plot(
+ lambda x: normal_pdf(x, mu_h, sigma_h),
+ x_range=[-10, 10],
+ color=BLUE_D,
+ stroke_width=6,
+ )
+ agent_curve = axis.plot(
+ lambda x: normal_pdf(x, mu_a, sigma_a),
+ x_range=[-10, 10],
+ color=RED_C,
+ stroke_width=6,
+ )
+ h_label = Text("human", font_size=22, color=BLUE_D).move_to(
+ axis.c2p(-6.4, 0.108)
+ )
+ a_label = Text("agent", font_size=22, color=RED_C).move_to(axis.c2p(5.8, 0.095))
+
+ boundary = DashedLine(
+ axis.c2p(0.0, 0.0), axis.c2p(0.0, 0.165), color=GREY_B, stroke_width=2
+ )
+ boundary_tag = Text("decision boundary", font_size=17, color=GREY_B).next_to(
+ boundary, UP, buff=0.08
+ )
+ boundary_tag.shift(RIGHT * 0.8)
+
+ g_obs = 1.6
+ g_line = Line(
+ axis.c2p(g_obs, 0.0),
+ axis.c2p(g_obs, 0.145),
+ color=HIGHLIGHT,
+ stroke_width=4,
+ )
+ g_dot = Dot(axis.c2p(g_obs, 0.145), color=HIGHLIGHT, radius=0.06)
+ g_tag = (
+ MathTex(r"g_{obs}", color=HIGHLIGHT)
+ .scale(0.72)
+ .next_to(g_dot, UP, buff=0.04)
+ )
+
+ self.play(FadeIn(axis), FadeIn(x_tag))
+ self.play(Create(human_curve), Create(agent_curve))
+ self.play(
+ FadeIn(h_label), FadeIn(a_label), FadeIn(boundary), FadeIn(boundary_tag)
+ )
+ self.play(FadeIn(g_line), FadeIn(g_dot), FadeIn(g_tag))
+
+ hint = MathTex(
+ r"g>0\ \Rightarrow\ \hat\alpha\ \text{upweights agent contamination}",
+ font_size=22,
+ color=GREY_B,
+ )
+ hint.next_to(x_tag, DOWN, buff=0.1)
+ hint.match_x(axis)
+ self.play(FadeIn(hint, shift=UP * 0.1))
+ self.wait(1.0)
+
+
+class ContaminationGeneratorScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Contamination Generator G(alpha)")
+ self.play(Write(title))
+
+ human_pool = card("labeled human sessions", color=BLUE_D, width=4.1)
+ agent_pool = card("synthetic agent sessions", color=RED_C, width=4.1)
+ mixed_pool = card("mixed batch for training", color=HIGHLIGHT, width=4.4)
+
+ top = (
+ VGroup(human_pool, agent_pool)
+ .arrange(RIGHT, buff=1.1)
+ .next_to(title, DOWN, buff=0.55)
+ )
+ mixed_pool.next_to(top, DOWN, buff=1.25)
+
+ a1 = Arrow(
+ human_pool.get_bottom(),
+ mixed_pool.get_top() + LEFT * 1.0,
+ buff=0.1,
+ stroke_width=4,
+ )
+ a2 = Arrow(
+ agent_pool.get_bottom(),
+ mixed_pool.get_top() + RIGHT * 1.0,
+ buff=0.1,
+ stroke_width=4,
+ )
+
+ self.play(FadeIn(top, shift=UP * 0.12), FadeIn(mixed_pool, shift=UP * 0.12))
+ self.play(FadeIn(a1), FadeIn(a2))
+
+ flow = VGroup(top, mixed_pool, a1, a2)
+ self.play(flow.animate.scale(0.68).to_edge(LEFT).shift(UP * 0.58))
+
+ alpha_tracker = ValueTracker(0.18)
+ bar_outline = Rectangle(
+ width=7.0, height=0.46, stroke_color=AXIS_INK, stroke_width=2
+ ).move_to(RIGHT * 0.55 + DOWN * 0.12)
+ base_h = Rectangle(
+ width=7.0, height=0.4, stroke_width=0, fill_color=BLUE_D, fill_opacity=0.35
+ ).move_to(bar_outline)
+
+ def make_agent_fill() -> Rectangle:
+ width = max(0.02, 7.0 * alpha_tracker.get_value())
+ rect = Rectangle(
+ width=width,
+ height=0.4,
+ stroke_width=0,
+ fill_color=RED_C,
+ fill_opacity=0.68,
+ )
+ rect.move_to(bar_outline.get_right() + LEFT * (width / 2.0))
+ return rect
+
+ agent_fill = always_redraw(make_agent_fill)
+ alpha_label = Text("alpha =", font_size=24).next_to(
+ bar_outline, DOWN, buff=0.16
+ )
+ alpha_value = always_redraw(
+ lambda: DecimalNumber(
+ alpha_tracker.get_value(),
+ num_decimal_places=2,
+ font_size=28,
+ color=HIGHLIGHT,
+ ).next_to(alpha_label, RIGHT, buff=0.1)
+ )
+ left_tag = Text("human share (1-alpha)", font_size=18, color=BLUE_D).next_to(
+ bar_outline, LEFT, buff=0.15
+ )
+ right_tag = Text("agent share (alpha)", font_size=18, color=RED_C).next_to(
+ bar_outline, RIGHT, buff=0.15
+ )
+
+ self.play(FadeIn(bar_outline), FadeIn(base_h), FadeIn(agent_fill))
+ self.play(
+ FadeIn(alpha_label),
+ FadeIn(alpha_value),
+ FadeIn(left_tag),
+ FadeIn(right_tag),
+ )
+
+ mix_eq = MathTex(
+ r"\hat Q(p\mid\tau')=(1-\alpha)\,\hat Q_H(p\mid\tau')+\alpha\,\hat Q_A(p\mid\tau')",
+ font_size=31,
+ ).next_to(bar_outline, DOWN, buff=0.45)
+ interval = MathTex(
+ r"\alpha\in[\alpha_0-\epsilon_\alpha,\,\alpha_0+\epsilon_\alpha]",
+ font_size=31,
+ color=GREY_B,
+ )
+ interval.next_to(mix_eq, DOWN, buff=0.2)
+ self.play(Write(mix_eq), Write(interval))
+
+ self.play(alpha_tracker.animate.set_value(0.32), run_time=1.2)
+ self.play(alpha_tracker.animate.set_value(0.55), run_time=1.2)
+ self.play(alpha_tracker.animate.set_value(0.24), run_time=1.1)
+ self.wait(0.9)
+
+
+class RobustControlScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Distributionally Robust Control Layer")
+ self.play(Write(title))
+
+ objective = MathTex(
+ r"\pi^*=\arg\max_\pi\min_{Q\in\mathcal U_\epsilon}\mathbb E_{d\sim Q}[R(p,d)-\lambda\,\mathrm{COI}_{\mathrm{leak}}(p,\tau') ]",
+ font_size=29,
+ ).next_to(title, DOWN, buff=0.4)
+ reward = MathTex(
+ r"r_t=R(p_t,d_t)-\lambda f(\tau_t')c_{info},\quad d_t\sim Q(\cdot\mid p_t,\tau_t')",
+ font_size=29,
+ color=HIGHLIGHT,
+ )
+ reward.next_to(objective, DOWN, buff=0.22)
+ demand_link = MathTex(
+ r"\hat Q(p_t,\tau_t')=\mathbb E_Q[d_t\mid p_t,\tau_t']",
+ font_size=27,
+ color=GREY_B,
+ ).next_to(reward, DOWN, buff=0.14)
+ self.play(Write(objective), Write(reward), Write(demand_link))
+ self.play(Circumscribe(objective, color=HIGHLIGHT, run_time=1.05))
+
+ plane = (
+ Axes(
+ x_range=[-3, 3, 1],
+ y_range=[-3, 3, 1],
+ x_length=5.6,
+ y_length=5.6,
+ tips=False,
+ axis_config={"stroke_width": 1.8, "color": AXIS_INK},
+ )
+ .to_edge(LEFT)
+ .shift(DOWN * 0.55)
+ )
+ center = Dot(plane.c2p(0, 0), color=BLUE_D, radius=0.08)
+ center_tag = (
+ MathTex(r"\hat P_N", color=BLUE_D)
+ .scale(0.75)
+ .next_to(center, UP, buff=0.07)
+ )
+ ball = Circle(radius=1.75, color=HIGHLIGHT, stroke_width=3).move_to(center)
+ ball_tag = (
+ MathTex(r"\mathcal U_\epsilon", color=HIGHLIGHT)
+ .scale(0.72)
+ .next_to(ball, UP, buff=0.08)
+ )
+
+ q1 = Dot(plane.c2p(1.0, 0.7), color=GREEN_C)
+ q2 = Dot(plane.c2p(-1.2, 0.9), color=RED_C)
+ q3 = Dot(plane.c2p(0.3, -1.3), color=GREEN_C)
+ q4 = Dot(plane.c2p(-0.9, -0.6), color=GREEN_C)
+ q2_tag = Text("worst-case Q*", font_size=18, color=RED_C).next_to(
+ q2, UP, buff=0.07
+ )
+
+ self.play(FadeIn(plane), FadeIn(center), FadeIn(center_tag))
+ self.play(Create(ball), FadeIn(ball_tag))
+ self.play(
+ LaggedStart(*[FadeIn(dot) for dot in [q1, q2, q3, q4]], lag_ratio=0.14)
+ )
+ self.play(FadeIn(q2_tag, shift=UP * 0.08))
+
+ inner_step = card(
+ "inner min picks Q*", color=RED_C, width=4.6, height=0.9, font_size=20
+ )
+ demand_step = card(
+ "sample demand from Q*", color=ORANGE, width=4.6, height=0.9, font_size=20
+ )
+ update_step = card(
+ "outer max updates policy",
+ color=GREEN_C,
+ width=4.6,
+ height=0.9,
+ font_size=20,
+ )
+ pipeline = (
+ VGroup(inner_step, demand_step, update_step)
+ .arrange(DOWN, buff=0.32)
+ .to_edge(RIGHT)
+ .shift(DOWN * 0.95)
+ )
+ chooser = Arrow(
+ q2.get_right() + RIGHT * 0.15,
+ inner_step.get_left(),
+ buff=0.08,
+ color=RED_C,
+ stroke_width=4,
+ )
+ stage_arrow_1 = Arrow(
+ inner_step.get_bottom(),
+ demand_step.get_top(),
+ buff=0.08,
+ stroke_width=3.6,
+ )
+ stage_arrow_2 = Arrow(
+ demand_step.get_bottom(),
+ update_step.get_top(),
+ buff=0.08,
+ stroke_width=3.6,
+ )
+ feedback = CurvedArrow(
+ update_step.get_left() + DOWN * 0.12,
+ center.get_right() + UP * 0.15,
+ angle=0.92,
+ color=GREEN_C,
+ stroke_width=3.6,
+ )
+ self.play(FadeIn(pipeline, shift=LEFT * 0.15))
+ self.play(FadeIn(chooser))
+ self.play(FadeIn(stage_arrow_1), FadeIn(stage_arrow_2))
+ self.play(FadeIn(feedback))
+
+ note = MathTex(
+ r"r_t\ \text{uses }d_t\sim Q^\star\ \text{(inner min)}",
+ font_size=24,
+ color=GREY_B,
+ )
+ note.to_edge(DOWN)
+ self.play(FadeIn(note, shift=UP * 0.1))
+ self.wait(1.0)
+
+
+class SystemLoopScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Online + Offline Defense Loop")
+ self.play(Write(title))
+
+ web = card("Web app", color=BLUE_D, width=2.9)
+ provider = card("Pricing provider", color=BLUE_D, width=3.5)
+ kafka = card("Kafka streams", color=HIGHLIGHT, width=3.1)
+ kernels = card("Kernel + KL estimator", color=GREEN_C, width=3.9)
+ generator = card("Generator G(alpha)", color=GREEN_C, width=3.5)
+ policy = card("DR-RL trainer", color=ORANGE, width=3.0)
+
+ web.move_to(LEFT * 4.6 + UP * 1.35)
+ provider.move_to(RIGHT * 4.2 + UP * 1.35)
+ kafka.move_to(LEFT * 4.6 + DOWN * 1.1)
+ kernels.move_to(LEFT * 1.3 + DOWN * 1.1)
+ generator.move_to(RIGHT * 2.0 + DOWN * 1.1)
+ policy.move_to(RIGHT * 5.1 + DOWN * 1.1)
+
+ online_tag = Text("online serving", font_size=22, weight="BOLD", color=GREY_B)
+ online_tag.next_to(web, UP, buff=0.38).align_to(web, LEFT)
+ offline_tag = Text(
+ "offline defense training", font_size=22, weight="BOLD", color=GREY_B
+ )
+ offline_tag.next_to(kafka, UP, buff=0.38).align_to(kafka, LEFT)
+
+ request_arrow = CurvedArrow(
+ web.get_right() + UP * 0.2,
+ provider.get_left() + UP * 0.2,
+ angle=-0.24,
+ stroke_width=4,
+ )
+ response_arrow = CurvedArrow(
+ provider.get_left() + DOWN * 0.2,
+ web.get_right() + DOWN * 0.2,
+ angle=-0.24,
+ stroke_width=4,
+ )
+ log_arrow = Arrow(web.get_bottom(), kafka.get_top(), buff=0.08, stroke_width=4)
+ k_to_kl = Arrow(kafka.get_right(), kernels.get_left(), buff=0.1, stroke_width=4)
+ kl_to_g = Arrow(
+ kernels.get_right(), generator.get_left(), buff=0.1, stroke_width=4
+ )
+ g_to_pi = Arrow(
+ generator.get_right(), policy.get_left(), buff=0.1, stroke_width=4
+ )
+ pi_to_provider = Arrow(
+ policy.get_top(), provider.get_bottom(), buff=0.08, stroke_width=4
+ )
+
+ nodes = VGroup(web, provider, kafka, kernels, generator, policy)
+ self.play(
+ FadeIn(online_tag, shift=UP * 0.08), FadeIn(offline_tag, shift=UP * 0.08)
+ )
+ self.play(
+ LaggedStart(
+ *[FadeIn(node, shift=UP * 0.08) for node in nodes], lag_ratio=0.12
+ )
+ )
+ self.play(
+ LaggedStart(
+ *[
+ FadeIn(a)
+ for a in [
+ request_arrow,
+ response_arrow,
+ log_arrow,
+ k_to_kl,
+ kl_to_g,
+ g_to_pi,
+ pi_to_provider,
+ ]
+ ],
+ lag_ratio=0.08,
+ )
+ )
+
+ labels = VGroup(
+ Text("request quote", font_size=17).next_to(request_arrow, UP, buff=0.06),
+ Text("serve price", font_size=17).next_to(response_arrow, DOWN, buff=0.06),
+ Text("events + quote logs", font_size=17).next_to(
+ log_arrow, RIGHT, buff=0.08
+ ),
+ Text("fit kernels + alpha", font_size=17).next_to(kl_to_g, UP, buff=0.08),
+ Text("robust policy train", font_size=17).next_to(g_to_pi, UP, buff=0.08),
+ Text("publish model", font_size=17).next_to(
+ pi_to_provider, RIGHT, buff=0.08
+ ),
+ )
+ self.play(LaggedStart(*[FadeIn(l) for l in labels], lag_ratio=0.15))
+ self.wait(1.0)
+
+
+class ObjectiveAndResultsScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Experimental Signal (paired benchmarks)")
+ self.play(Write(title))
+
+ # Paired robust vs non-robust cohort; magnitudes align with sweep-scale logs.
+ objective_chart = BarChart(
+ values=[3.41, 3.91],
+ bar_names=["robust", "non-robust"],
+ y_range=[0, 5, 1],
+ y_length=2.9,
+ x_length=4.8,
+ bar_colors=[GREEN_C, RED_C],
+ )
+ objective_label = MathTex(
+ r"\text{objective}\ (\times 10^5)", font_size=22
+ ).next_to(objective_chart, UP, buff=0.1)
+
+ revenue_chart = BarChart(
+ values=[3.80, 4.18],
+ bar_names=["robust", "non-robust"],
+ y_range=[0, 5, 1],
+ y_length=2.9,
+ x_length=4.8,
+ bar_colors=[GREEN_C, RED_C],
+ )
+ revenue_label = MathTex(
+ r"\text{revenue}\ (\times 10^5)", font_size=22
+ ).next_to(revenue_chart, UP, buff=0.1)
+
+ charts = VGroup(
+ VGroup(objective_label, objective_chart),
+ VGroup(revenue_label, revenue_chart),
+ ).arrange(RIGHT, buff=0.85)
+ charts.next_to(title, DOWN, buff=0.7)
+ self.play(FadeIn(charts, shift=UP * 0.2))
+
+ pairwise = MathTex(
+ r"\textbf{wins:}\quad \tfrac{13}{40}\ (\text{obj}),\ \tfrac{16}{40}\ (\text{rev})",
+ font_size=26,
+ )
+ pairwise.next_to(charts, DOWN, buff=0.32)
+ self.play(FadeIn(pairwise, shift=RIGHT * 0.12))
+
+ caution = MathTex(
+ r"\text{regime-dependent};\ \text{read with COI + stability (Results)}",
+ font_size=22,
+ color=GREY_B,
+ )
+ caution.to_edge(DOWN)
+ self.play(FadeIn(caution, shift=UP * 0.1))
+ self.wait(1.1)
+
+
+class ThesisBannerPosterScene(Scene):
+ def construct(self) -> None:
+ title = Text("PHANTOM", font_size=72, weight="BOLD", color=INK).to_edge(UP)
+ subtitle = Text(
+ "Pricing Heuristics Against Non-human Transaction Orchestration",
+ font_size=24,
+ color=GREY_B,
+ ).next_to(title, DOWN, buff=0.05)
+
+ coi_axes = Axes(
+ x_range=[0, 1, 0.2],
+ y_range=[0, 1, 0.2],
+ x_length=3.15,
+ y_length=1.75,
+ tips=False,
+ axis_config={"stroke_width": 1.8, "color": AXIS_INK},
+ )
+ coi_n1 = coi_axes.plot(
+ lambda x: (1 - x) ** 1,
+ x_range=[0, 1],
+ color=BLUE_D,
+ stroke_width=4,
+ )
+ coi_n8 = coi_axes.plot(
+ lambda x: (1 - x) ** 8,
+ x_range=[0, 1],
+ color=ORANGE,
+ stroke_width=4,
+ )
+ coi_hint = Text(
+ "Order-statistic tail compresses as query count grows", font_size=15
+ )
+ coi_hint.set_color(GREY_B).next_to(coi_axes, DOWN, buff=0.06)
+ coi_title = Text("1) COI erosion", font_size=23, weight="BOLD", color=ORANGE)
+ coi_body = VGroup(coi_axes, coi_n1, coi_n8, coi_hint)
+ coi_group = VGroup(coi_title, coi_body).arrange(DOWN, buff=0.08)
+ coi_frame = SurroundingRectangle(coi_group, color=ORANGE, buff=0.14)
+ coi_frame.set_fill(color=ORANGE, opacity=0.05)
+ coi_panel = VGroup(coi_frame, coi_group)
+
+ gap_axes = Axes(
+ x_range=[-8, 8, 2],
+ y_range=[0.0, 0.2, 0.05],
+ x_length=3.15,
+ y_length=1.75,
+ tips=False,
+ axis_config={"stroke_width": 1.8, "color": AXIS_INK},
+ )
+ gap_h = gap_axes.plot(
+ lambda x: normal_pdf(x, -3.35, 2.67),
+ x_range=[-8, 8],
+ color=BLUE_D,
+ stroke_width=4,
+ )
+ gap_a = gap_axes.plot(
+ lambda x: normal_pdf(x, 1.65, 2.83),
+ x_range=[-8, 8],
+ color=RED_C,
+ stroke_width=4,
+ )
+ gap_boundary = DashedLine(
+ gap_axes.c2p(0, 0),
+ gap_axes.c2p(0, 0.17),
+ color=GREY_B,
+ stroke_width=2,
+ )
+ gap_hint = Text(
+ "Gap score g = Delta_H - Delta_A drives alpha-hat", font_size=15
+ )
+ gap_hint.set_color(GREY_B).next_to(gap_axes, DOWN, buff=0.06)
+ gap_title = Text(
+ "2) Behavioral separability", font_size=23, weight="BOLD", color=GREEN_C
+ )
+ gap_body = VGroup(gap_axes, gap_h, gap_a, gap_boundary, gap_hint)
+ gap_group = VGroup(gap_title, gap_body).arrange(DOWN, buff=0.08)
+ gap_frame = SurroundingRectangle(gap_group, color=GREEN_C, buff=0.14)
+ gap_frame.set_fill(color=GREEN_C, opacity=0.05)
+ gap_panel = VGroup(gap_frame, gap_group)
+
+ ctrl_title = Text(
+ "3) Robust pricing control", font_size=23, weight="BOLD", color=HIGHLIGHT
+ )
+ ctrl_signal = MathTex(r"\hat\alpha(\tau')=\sigma(\beta g)", font_size=31)
+ ctrl_policy = MathTex(
+ r"\pi^*=\arg\max_\pi\min_{Q\in\mathcal U_\epsilon}\mathbb E[r]",
+ font_size=29,
+ color=HIGHLIGHT,
+ )
+ ctrl_steps = VGroup(
+ card(
+ "estimate contamination from behavior",
+ color=GREEN_C,
+ width=4.0,
+ height=0.72,
+ font_size=16,
+ ),
+ card(
+ "optimize price policy under uncertainty",
+ color=ORANGE,
+ width=4.0,
+ height=0.72,
+ font_size=16,
+ ),
+ ).arrange(DOWN, buff=0.18)
+ ctrl_arrow = Arrow(
+ ctrl_steps[0].get_bottom(),
+ ctrl_steps[1].get_top(),
+ buff=0.06,
+ color=AXIS_INK,
+ stroke_width=3,
+ )
+ ctrl_body = VGroup(ctrl_signal, ctrl_policy, ctrl_steps, ctrl_arrow).arrange(
+ DOWN, buff=0.14
+ )
+ ctrl_group = VGroup(ctrl_title, ctrl_body).arrange(DOWN, buff=0.08)
+ ctrl_frame = SurroundingRectangle(ctrl_group, color=HIGHLIGHT, buff=0.14)
+ ctrl_frame.set_fill(color=HIGHLIGHT, opacity=0.05)
+ ctrl_panel = VGroup(ctrl_frame, ctrl_group)
+
+ panels = VGroup(coi_panel, gap_panel, ctrl_panel).arrange(RIGHT, buff=0.3)
+ panels.scale(0.92).next_to(subtitle, DOWN, buff=0.28)
+
+ web = card("web sessions", color=BLUE_D, width=2.2, height=0.7, font_size=17)
+ kafka = card(
+ "quote + event logs", color=HIGHLIGHT, width=2.6, height=0.7, font_size=17
+ )
+ kernel = card(
+ "transition kernels", color=GREEN_C, width=2.5, height=0.7, font_size=17
+ )
+ policy = card(
+ "robust policy", color=ORANGE, width=2.2, height=0.7, font_size=17
+ )
+ flow_nodes = VGroup(web, kafka, kernel, policy).arrange(RIGHT, buff=0.22)
+ flow_nodes.to_edge(DOWN, buff=0.52)
+ flow_arrows = VGroup(
+ Arrow(web.get_right(), kafka.get_left(), buff=0.05, stroke_width=2.8),
+ Arrow(kafka.get_right(), kernel.get_left(), buff=0.05, stroke_width=2.8),
+ Arrow(kernel.get_right(), policy.get_left(), buff=0.05, stroke_width=2.8),
+ )
+
+ status = VGroup(
+ Text("Mann-Whitney p = 0.0006", font_size=19, color=GREEN_C),
+ Text("Pairwise robust wins: 13/40 objective, 16/40 revenue", font_size=19),
+ ).arrange(DOWN, buff=0.06)
+ status[1].set_color(GREY_B)
+ status.next_to(flow_nodes, UP, buff=0.15)
+
+ footer = Text(
+ "From mechanism failure to an implementable defense loop",
+ font_size=25,
+ color=HIGHLIGHT,
+ ).next_to(flow_nodes, DOWN, buff=0.13)
+
+ self.add(
+ title,
+ subtitle,
+ panels,
+ flow_nodes,
+ flow_arrows,
+ status,
+ footer,
+ )
+ self.wait(0.1)
+
+
+class RewardAndLeakageScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Reward: Revenue vs COI Leakage")
+ self.play(Write(title))
+
+ leak = MathTex(
+ r"\mathrm{COI}_{\mathrm{leak}}(p,\tau')=f(\tau')\cdot\mathrm{InfoValue}(p,\tau')}",
+ font_size=34,
+ color=HIGHLIGHT,
+ ).next_to(title, DOWN, buff=0.42)
+ self.play(Write(leak))
+ self.play(Circumscribe(leak, color=HIGHLIGHT, run_time=1.1))
+
+ info = MathTex(
+ r"\mathrm{InfoValue}\in\{1,\,-\log\pi(p\mid\tau')\}",
+ font_size=30,
+ color=GREY_B,
+ ).next_to(leak, DOWN, buff=0.22)
+ self.play(Write(info))
+
+ f_track = ValueTracker(0.0)
+ bar_w, bar_h = 4.2, 0.38
+ bar_bg = Rectangle(
+ width=bar_w,
+ height=bar_h,
+ stroke_color=AXIS_INK,
+ stroke_width=2,
+ fill_opacity=0.06,
+ ).shift(DOWN * 0.75)
+
+ def leak_bar() -> Rectangle:
+ w = max(0.04, bar_w * float(f_track.get_value()))
+ r = Rectangle(
+ width=w,
+ height=bar_h - 0.06,
+ stroke_width=0,
+ fill_color=RED_C,
+ fill_opacity=0.55,
+ )
+ r.move_to(bar_bg.get_left() + RIGHT * (w / 2.0) + RIGHT * 0.02)
+ return r
+
+ leak_fill = always_redraw(leak_bar)
+ scale_lbl = MathTex(
+ r"\lambda\cdot\mathrm{COI}_{\mathrm{leak}}",
+ font_size=28,
+ ).next_to(bar_bg, UP, buff=0.18)
+ f_readout = always_redraw(
+ lambda: MathTex(
+ rf"f(\tau')={f_track.get_value():.2f}",
+ font_size=32,
+ color=HIGHLIGHT,
+ ).next_to(bar_bg, DOWN, buff=0.2)
+ )
+
+ self.play(FadeIn(bar_bg), Write(scale_lbl))
+ self.add(leak_fill, f_readout)
+ self.play(f_track.animate.set_value(0.88), run_time=2.4, rate_func=smooth)
+ self.wait(0.25)
+
+ objective = MathTex(
+ r"\pi^*=\arg\max_\pi\min_{Q\in\mathcal{U}_\epsilon}\mathbb{E}_{d\sim Q}\Big["
+ r"R(p,d)-\lambda\,\mathrm{COI}_{\mathrm{leak}}(p,\tau')\Big]",
+ font_size=28,
+ ).to_edge(DOWN, buff=0.32)
+ self.play(Write(objective))
+ self.play(Indicate(objective, color=HIGHLIGHT, run_time=1.0))
+ self.wait(0.7)
+
+
+class StackelbergAmbiguityScene(Scene):
+ def construct(self) -> None:
+ title = scene_title("Stackelberg Step + Contamination Ambiguity")
+ self.play(Write(title))
+
+ stack = VGroup(
+ MathTex(r"\text{leader: platform chooses }p_t", font_size=32),
+ MathTex(
+ r"\text{follower: sessions }(d_t,\tau_t')\text{ under }Q(\cdot\mid p_t,\tau_t')",
+ font_size=28,
+ color=GREY_B,
+ ),
+ ).arrange(DOWN, aligned_edge=LEFT, buff=0.2)
+ stack.next_to(title, DOWN, buff=0.38).align_to(title, LEFT)
+ self.play(LaggedStart(*[Write(line) for line in stack], lag_ratio=0.2))
+
+ nl = NumberLine(
+ x_range=[0, 1, 0.2],
+ length=7.2,
+ color=AXIS_INK,
+ include_numbers=True,
+ decimal_number_config={"num_decimal_places": 1, "color": GREY_B},
+ ).shift(DOWN * 0.55)
+
+ alpha0, eps_a = 0.35, 0.12
+ lo, hi = max(0.0, alpha0 - eps_a), min(1.0, alpha0 + eps_a)
+
+ self.play(Create(nl))
+ tick0 = Line(
+ nl.n2p(alpha0) + UP * 0.12,
+ nl.n2p(alpha0) + DOWN * 0.12,
+ color=HIGHLIGHT,
+ stroke_width=4,
+ )
+ interval = Line(
+ nl.n2p(lo),
+ nl.n2p(hi),
+ color=HIGHLIGHT,
+ stroke_width=10,
+ )
+ self.play(Create(tick0), Create(interval))
+
+ amb = MathTex(
+ r"\mathcal{A}_{\epsilon_\alpha}(\alpha_0)=\{\alpha:\lvert\alpha-\alpha_0\rvert\le\epsilon_\alpha\}",
+ font_size=30,
+ ).next_to(nl, DOWN, buff=0.42)
+ self.play(Write(amb))
+ self.play(Circumscribe(interval, color=HIGHLIGHT, run_time=0.9))
+ self.wait(0.6)
+
+
+SCENE_ORDER = [
+ "DefenseOpening",
+ "CardMarketAnalogyScene",
+ "COIFirstPrinciplesScene",
+ "COIOrderStatisticProofScene",
+ "BehaviorKernelConstructionScene",
+ "SeparabilitySignalScene",
+ "ContaminationGeneratorScene",
+ "RewardAndLeakageScene",
+ "StackelbergAmbiguityScene",
+ "RobustControlScene",
+ "SystemLoopScene",
+ "ObjectiveAndResultsScene",
+]
+
+POSTER_SCENES = ["ThesisBannerPosterScene"]
diff --git a/paper/defense/manim/scripts/ffmpeg_concat_defense.sh b/paper/defense/manim/scripts/ffmpeg_concat_defense.sh
new file mode 100755
index 0000000..72948a4
--- /dev/null
+++ b/paper/defense/manim/scripts/ffmpeg_concat_defense.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# Concatenate rendered defense scenes (all under media/videos/defense//).
+# Usage from paper/defense/manim after: ./render_defense full --quality qh
+# ./scripts/ffmpeg_concat_defense.sh qh
+set -euo pipefail
+QUALITY="${1:-qm}"
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+LIST="$(mktemp)"
+trap 'rm -f "$LIST"' EXIT
+DIR="$ROOT/media/videos/defense/$QUALITY"
+
+while IFS= read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ ^#.*$ || -z "${line// }" ]] && continue
+ name="$line"
+ f="$DIR/${name}.mp4"
+ if [[ ! -f "$f" ]]; then
+ echo "missing: $f" >&2
+ exit 1
+ fi
+ echo "file '$f'" >>"$LIST"
+done <"$ROOT/defense_scene_order.txt"
+
+OUT="$ROOT/media/defense_rehearsal_${QUALITY}.mp4"
+ffmpeg -y -f concat -safe 0 -i "$LIST" -c copy "$OUT"
+echo "wrote $OUT"
diff --git a/scripts/nx_research.sh b/scripts/nx_research.sh
index 4cc39ee..6460703 100644
--- a/scripts/nx_research.sh
+++ b/scripts/nx_research.sh
@@ -120,8 +120,8 @@ case "$cmd" in
python3 - <<'PY'
from pathlib import Path
-skip = {"node_modules", ".venv", "venv"}
-exts = {".ts", ".py"}
+skip = {"node_modules", ".venv", "venv", ".venv-ray"}
+exts = {".ts", ".py", ".ipynb"}
total = 0
for path in Path(".").rglob("*"):
if not path.is_file() or path.suffix not in exts or any(part in skip for part in path.parts):