diff --git a/.gitignore b/.gitignore index 1107134..1d4d00b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,8 +64,8 @@ tests/e2e/test-results/ tests/e2e/node_modules/** # rl/sim run outputs -sim/rl/behavior_loader/*.dot -sim/rl/behavior_loader/*.png +# sim/rl/behavior_loader/*.dot +# sim/rl/behavior_loader/*.png sim/rl/behavior_loader/*.svg sim/rl/behavior_loader/*.pdf sim/rl/runs/ diff --git a/Makefile b/Makefile index cc9711f..02a8755 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ SWEEP_ENV_LOAD = set -a; [ -f "$(SWEEP_ENV_FILE)" ] && . "$(SWEEP_ENV_FILE)" || .PHONY: help help: - @echo "pdf.build pdf.watch pdf.clean pdf.genpop pdf.genpop.watch pdf.summary pdf.summary.watch pdf.arxiv | test.backend test.e2e test.all | web.dev | install | train | benchmark | benchmark.simple | benchmark.agent | train.agent | train.bootstrap | stats.lines | docs.platform | manim.defense manim.defense.hq manim.render manim.render.full manim.render.poster manim.render.appendix manim.render.all" + @echo "pdf.build pdf.watch pdf.clean pdf.genpop pdf.genpop.watch pdf.summary pdf.summary.watch pdf.arxiv pdf.defense pdf.defense.html | test.backend test.e2e test.all | web.dev | install | train | benchmark | benchmark.simple | benchmark.agent | train.agent | train.bootstrap | stats.lines | docs.platform | manim.defense manim.defense.hq manim.render manim.render.full manim.render.poster manim.render.appendix manim.render.all" @echo "backend.server backend.provider backend.worker | platform.up platform.down platform.logs | docker.train.publish" @echo "data.pull data.push data.whoclicked.publish | study.margin-erosion study.margin-erosion.quick study.margin-erosion.plot" @echo "tpu.ray.bootstrap tpu.ray.deps tpu.ray.verify tpu.ray.teardown" @@ -110,6 +110,14 @@ pdf.summary: pdf.summary.watch: @bash scripts/nx_paper.sh watch-summary +.PHONY: pdf.defense +pdf.defense: + @cd paper/defense && pdflatex -interaction=nonstopmode defense.tex && pdflatex -interaction=nonstopmode defense.tex + +.PHONY: pdf.defense.html +pdf.defense.html: + @bash paper/defense/build_html.sh + .PHONY: test.backend test.backend: @$(NX) run research:test diff --git a/README.md b/README.md index cc4a8aa..dbac205 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -

- PHANTOM logo -

+ + + -# PHANTOM +![](./banner.png) Agent-aware dynamic pricing research platform for studying how automated transaction orchestration changes pricing power, and for testing defenses that recover margin while protecting legitimate user experience. diff --git a/banner.png b/banner.png new file mode 100644 index 0000000..e1d7ec5 Binary files /dev/null and b/banner.png differ diff --git a/docs/index.html b/docs/index.html index 7aa0c21..8f9288e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -45,8 +45,8 @@ - - + + @@ -59,12 +59,19 @@ PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms - Daniel Rösel | Academic Research + - + + + + + + + @@ -236,92 +243,87 @@
-
+
-
-
-

Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms

- +
+
+

IE University Bachelor's Thesis · 2025

+

PHANTOM

+

Revenue management in the age of AI agents.

-
- IE University
Bachelor's Thesis 2025
- Advisor: Alberto Martín Izquierdo -
+
+ Private Valuation + True Demand + Constraints +
-
- + +
-
+
+
+
+
+
+ 01 +

The vulnerability

+

Repeated agent price queries collapse the Cost of Information that dynamic pricing depends on.

+
+
+ 02 +

The signal

+

Human and agent sessions separate through transition-kernel behavior, not brittle bot flags.

+
+
+ 03 +

The defense

+

Distributionally robust RL preserves pricing power under contaminated demand.

+
+
+
+
-
+
@@ -337,7 +339,7 @@
-

Abstract

+

The thesis, compressed.

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 H and 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 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.

+
+
+
+ Isometric illustration of a human user as a cube character +
+

Users

+

Have new needs and means of research & acquisition.

+
+
+
+ Isometric illustration of an LLM agent depicted as a cube robot +
+

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.

+
    +
  1. + 01 + We can distinguish humans from agents at the transition-kernel level.Mann–Whitney U = 2.0, p = 0.0006 across 29 labeled sessions. +
  2. +
  3. + 02 + Revenue declines monotonically in agent-contaminated systems.Each 1.0 step of contamination α removes ~90,140 in cohort revenue (p < 10-77). +
  4. +
  5. + 03 + Distributionally robust RL preserves margin under worst-case contamination.Defended policy holds positive COI gap over baseline across α ∈ [0, 1]. +
  6. +
+

Our solution can be forward-deployed to any e-commerce platform to preserve their COI.

+
+ +
+

WhoClickedIt — published on Hugging Face.

+

~4k rows of labeled human and agent interaction data across hotel and airline tasks. Open dataset used for training the behavioral kernels.

+ huggingface.co/datasets/velocitatem/whoclickedit +
+
+
+
+ @@ -428,35 +499,7 @@ - -
-
-
-

Defense Scenes

- -
-
-
- + @@ -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(p1, ..., pN) - 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 = DKL(T̂' || T̄H) ΔA = DKL(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):