diff --git a/Makefile b/Makefile index cc9711f..82f932d 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 | 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,10 @@ 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: test.backend test.backend: @$(NX) run research:test 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..6d4bf92 --- /dev/null +++ b/paper/defense/defense.nav @@ -0,0 +1,125 @@ +\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/6}{}{0}} +\headcommand {\beamer@framepages {4}{6}} +\headcommand {\beamer@sectionpages {1}{6}} +\headcommand {\beamer@subsectionpages {1}{6}} +\headcommand {\sectionentry {1}{Platform Development}{7}{Platform Development}{0}} +\headcommand {\slideentry {1}{0}{1}{7/8}{}{0}} +\headcommand {\beamer@framepages {7}{8}} +\headcommand {\slideentry {1}{0}{2}{9/9}{}{0}} +\headcommand {\beamer@framepages {9}{9}} +\headcommand {\slideentry {1}{0}{3}{10/12}{}{0}} +\headcommand {\beamer@framepages {10}{12}} +\headcommand {\beamer@sectionpages {7}{12}} +\headcommand {\beamer@subsectionpages {7}{12}} +\headcommand {\sectionentry {2}{Distinguishability Construction}{13}{Distinguishability Construction}{0}} +\headcommand {\slideentry {2}{0}{1}{13/14}{}{0}} +\headcommand {\beamer@framepages {13}{14}} +\headcommand {\slideentry {2}{0}{2}{15/15}{}{0}} +\headcommand {\beamer@framepages {15}{15}} +\headcommand {\slideentry {2}{0}{3}{16/17}{}{0}} +\headcommand {\beamer@framepages {16}{17}} +\headcommand {\beamer@sectionpages {13}{17}} +\headcommand {\beamer@subsectionpages {13}{17}} +\headcommand {\sectionentry {3}{Distributionally Robust RL}{18}{Distributionally Robust RL}{0}} +\headcommand {\slideentry {3}{0}{1}{18/18}{}{0}} +\headcommand {\beamer@framepages {18}{18}} +\headcommand {\slideentry {3}{0}{2}{19/20}{}{0}} +\headcommand {\beamer@framepages {19}{20}} +\headcommand {\slideentry {3}{0}{3}{21/22}{}{0}} +\headcommand {\beamer@framepages {21}{22}} +\headcommand {\beamer@sectionpages {18}{22}} +\headcommand {\beamer@subsectionpages {18}{22}} +\headcommand {\sectionentry {4}{Results}{23}{Results}{0}} +\headcommand {\slideentry {4}{0}{1}{23/23}{}{0}} +\headcommand {\beamer@framepages {23}{23}} +\headcommand {\beamer@sectionpages {23}{23}} +\headcommand {\beamer@subsectionpages {23}{23}} +\headcommand {\sectionentry {5}{Conclusions}{24}{Conclusions}{0}} +\headcommand {\slideentry {5}{0}{1}{24/24}{}{0}} +\headcommand {\beamer@framepages {24}{24}} +\headcommand {\slideentry {5}{0}{2}{25/28}{}{0}} +\headcommand {\beamer@framepages {25}{28}} +\headcommand {\slideentry {5}{0}{3}{29/29}{}{0}} +\headcommand {\beamer@framepages {29}{29}} +\headcommand {\gdef \insertmainframenumber {17}} +\headcommand {\partentry {\translate {Appendix}}{1}} +\headcommand {\beamer@partpages {1}{29}} +\headcommand {\beamer@sectionpages {24}{29}} +\headcommand {\beamer@subsectionpages {24}{29}} +\headcommand {\beamer@appendixpages {30}} +\headcommand {\beamer@sectionpages {30}{29}} +\headcommand {\beamer@subsectionpages {30}{29}} +\headcommand {\sectionentry {6}{Appendix}{30}{Appendix}{1}} +\headcommand {\slideentry {6}{0}{1}{30/30}{}{1}} +\headcommand {\beamer@framepages {30}{30}} +\headcommand {\slideentry {6}{0}{2}{31/31}{}{1}} +\headcommand {\beamer@framepages {31}{31}} +\headcommand {\slideentry {6}{0}{3}{32/32}{}{1}} +\headcommand {\beamer@framepages {32}{32}} +\headcommand {\slideentry {6}{0}{4}{33/33}{}{1}} +\headcommand {\beamer@framepages {33}{33}} +\headcommand {\slideentry {6}{0}{5}{34/34}{}{1}} +\headcommand {\beamer@framepages {34}{34}} +\headcommand {\slideentry {6}{0}{6}{35/35}{}{1}} +\headcommand {\beamer@framepages {35}{35}} +\headcommand {\slideentry {6}{0}{7}{36/36}{}{1}} +\headcommand {\beamer@framepages {36}{36}} +\headcommand {\slideentry {6}{0}{8}{37/37}{}{1}} +\headcommand {\beamer@framepages {37}{37}} +\headcommand {\slideentry {6}{0}{9}{38/38}{}{1}} +\headcommand {\beamer@framepages {38}{38}} +\headcommand {\slideentry {6}{0}{10}{39/39}{}{1}} +\headcommand {\beamer@framepages {39}{39}} +\headcommand {\slideentry {6}{0}{11}{40/40}{}{1}} +\headcommand {\beamer@framepages {40}{40}} +\headcommand {\slideentry {6}{0}{12}{41/41}{}{1}} +\headcommand {\beamer@framepages {41}{41}} +\headcommand {\slideentry {6}{0}{13}{42/42}{}{1}} +\headcommand {\beamer@framepages {42}{42}} +\headcommand {\slideentry {6}{0}{14}{43/43}{}{1}} +\headcommand {\beamer@framepages {43}{43}} +\headcommand {\slideentry {6}{0}{15}{44/44}{}{1}} +\headcommand {\beamer@framepages {44}{44}} +\headcommand {\slideentry {6}{0}{16}{45/45}{}{1}} +\headcommand {\beamer@framepages {45}{45}} +\headcommand {\slideentry {6}{0}{17}{46/46}{}{1}} +\headcommand {\beamer@framepages {46}{46}} +\headcommand {\slideentry {6}{0}{18}{47/47}{}{1}} +\headcommand {\beamer@framepages {47}{47}} +\headcommand {\slideentry {6}{0}{19}{48/48}{}{1}} +\headcommand {\beamer@framepages {48}{48}} +\headcommand {\slideentry {6}{0}{20}{49/49}{}{1}} +\headcommand {\beamer@framepages {49}{49}} +\headcommand {\slideentry {6}{0}{21}{50/50}{}{1}} +\headcommand {\beamer@framepages {50}{50}} +\headcommand {\slideentry {6}{0}{22}{51/51}{}{1}} +\headcommand {\beamer@framepages {51}{51}} +\headcommand {\slideentry {6}{0}{23}{52/52}{}{1}} +\headcommand {\beamer@framepages {52}{52}} +\headcommand {\slideentry {6}{0}{24}{53/53}{}{1}} +\headcommand {\beamer@framepages {53}{53}} +\headcommand {\slideentry {6}{0}{25}{54/54}{}{1}} +\headcommand {\beamer@framepages {54}{54}} +\headcommand {\slideentry {6}{0}{26}{55/55}{}{1}} +\headcommand {\beamer@framepages {55}{55}} +\headcommand {\slideentry {6}{0}{27}{56/56}{}{1}} +\headcommand {\beamer@framepages {56}{56}} +\headcommand {\slideentry {6}{0}{28}{57/57}{}{1}} +\headcommand {\beamer@framepages {57}{57}} +\headcommand {\slideentry {6}{0}{29}{58/58}{}{1}} +\headcommand {\beamer@framepages {58}{58}} +\headcommand {\slideentry {6}{0}{30}{59/59}{}{1}} +\headcommand {\beamer@framepages {59}{59}} +\headcommand {\beamer@partpages {30}{59}} +\headcommand {\beamer@subsectionpages {30}{59}} +\headcommand {\beamer@sectionpages {30}{59}} +\headcommand {\beamer@documentpages {59}} +\headcommand {\gdef \inserttotalframenumber {30}} +\headcommand {\gdef \inserttotalframenumber {17}} +\headcommand {\gdef \appendixtotalframenumber {30}} diff --git a/paper/defense/defense.pdf b/paper/defense/defense.pdf new file mode 100644 index 0000000..672e5ba 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..24e5fce --- /dev/null +++ b/paper/defense/defense.tex @@ -0,0 +1,559 @@ +% Final thesis defense (PHANTOM) +% Build: cd paper/defense && pdflatex defense.tex && pdflatex defense.tex +\documentclass[aspectratio=169,11pt]{beamer} + +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{microtype} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{booktabs} +\usepackage{appendixnumberbeamer} +\usepackage{hyperref} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,calc,positioning,fit,shapes.geometric,shapes.misc} + +\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] + +% Palette +\definecolor{PhantomPaper}{HTML}{F6F1E9} +\definecolor{PhantomInk}{HTML}{0F1B2D} +\definecolor{PhantomSlate}{HTML}{24364C} +\definecolor{PhantomCyan}{HTML}{C97A3D} +\definecolor{PhantomIndigo}{HTML}{2F8F8A} + +\setbeamercolor{normal text}{fg=PhantomSlate,bg=PhantomPaper} +\setbeamercolor{alerted text}{fg=PhantomCyan!95!black} +\setbeamercolor{example text}{fg=PhantomIndigo!95!black} +\setbeamercolor{palette primary}{fg=white,bg=PhantomInk} +\setbeamercolor{frametitle}{parent=palette primary} +\setbeamercolor{progress bar}{fg=PhantomCyan,bg=PhantomInk!28!white} +\setbeamercolor{title separator}{use=progress bar,parent=progress bar} +\setbeamercolor{structure}{fg=PhantomIndigo!90!black} +\setbeamercolor{block title}{fg=white,bg=PhantomIndigo!85!black} +\setbeamercolor{block body}{fg=PhantomSlate,bg=PhantomIndigo!8!white} +\setbeamercolor{alertblock title}{fg=white,bg=PhantomCyan!92!black} +\setbeamercolor{alertblock body}{fg=PhantomSlate,bg=PhantomCyan!10!white} +\setbeamercolor{exampleblock title}{fg=white,bg=PhantomIndigo!70!black} +\setbeamercolor{exampleblock body}{fg=PhantomSlate,bg=PhantomIndigo!8!white} + +\setbeamertemplate{navigation symbols}{} +\setbeamertemplate{itemize item}{\small\raise0.3ex\hbox{$\bullet$}} +\setbeamertemplate{itemize subitem}{\tiny\raise0.2ex\hbox{$\circ$}} + +\hypersetup{colorlinks=true,urlcolor=PhantomIndigo!90!black,linkcolor=PhantomSlate} + +\title{PHANTOM} +\subtitle{Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms} +\author{Daniel Rösel} +\institute{IE University, Madrid \\ Supervisor: Alberto Martín Izquierdo} +\date{\today} + +\titlegraphic{% + \begin{tikzpicture} + \shade[left color=PhantomCyan,right color=PhantomIndigo] (0,0) rectangle (0.55\paperwidth,0.06); + \end{tikzpicture}% +} + +\newcommand{\stagebar}[1]{} + +\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}% +} + +\begin{document} + +{ +\setbeamercolor{background canvas}{bg=PhantomInk} +\begin{frame}[plain] + \vfill + \centering + {\color{white}\Huge\bfseries PHANTOM\par} + \vspace{0.6em} + {\color{PhantomCyan}\rule{0.45\paperwidth}{0.06cm}\par} + \vspace{0.8em} + {\large\color{white!90!black}Pricing heuristics against non-human transaction orchestration\par} + \vfill + {\color{white!75!black}\normalsize Daniel Rösel\par} + {\color{white!65!black}\small IE University \textbullet\ Supervisor: Alberto Martín Izquierdo\par} + \vspace{1.2em} + {\footnotesize\color{PhantomCyan!80!white}\href{https://velocitatem.github.io/PHANTOM/}{\texttt{velocitatem.github.io/PHANTOM}}} + \vfill +\end{frame} +} + +\begin{frame}{Roadmap: one argument in six stages (15 min)} + \centering + \begin{tikzpicture}[ + font=\scriptsize\sffamily, + stage/.style={draw=PhantomInk,rounded corners=3pt,fill=PhantomCyan!10,minimum width=1.95cm,minimum height=1.05cm,align=center}, + flow/.style={-{Stealth[length=2.0mm,width=1.8mm]},line width=1pt,PhantomSlate} + ] + \node[stage,fill=PhantomCyan!14] (intro) {Intro\\2m}; + \node[stage,right=0.30cm of intro] (platform) {Platform\\4m}; + \node[stage,right=0.30cm of platform] (signal) {Signal\\4m}; + \node[stage,right=0.30cm of signal] (drrl) {DR-RL\\4m}; + \node[stage,right=0.30cm of drrl] (results) {Results\\1m}; + \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.75em} + \begin{block}{Main research question} + How can dynamic pricing preserve margin integrity when transactions are increasingly mediated by non-human agents? + \end{block} + \stagebar{1} +\end{frame} + +\begin{frame}{Agentic recon creates direct financial pressure on pricing power} + \centering + \begin{tikzpicture}[ + font=\small\sffamily, + flow/.style={draw=PhantomInk,rounded corners=6pt,minimum width=5.3cm,minimum height=1.25cm,align=center}, + note/.style={draw=PhantomInk!55,rounded corners=4pt,minimum width=11.2cm,minimum height=0.95cm,align=center,fill=white,font=\scriptsize} + ] + \node[flow,fill=PhantomCyan!18] (recon) at (-3.1,1.1) + {\textbf{Recon session}\\samples multiple quotes}; + \node[flow,fill=PhantomIndigo!16] (buy) at (3.1,1.1) + {\textbf{Clean execution session}\\buys using the best found quote}; + \draw[-{Stealth[length=3mm]},ultra thick,PhantomSlate] (recon.east) -- (buy.west); + \node[font=\scriptsize\bfseries,text=PhantomSlate] at (0,1.98) + {query and purchase are decoupled}; + \draw[densely dashed,thick,PhantomCyan!90!black] + (recon.south east) .. controls +(1.15,-0.95) and +(-1.15,-0.95) .. (buy.south west); + \node[note] at (0,-0.65) + {The platform sees behavior proxy $\hat q$, while true demand response $d(p\mid\theta)$ stays latent.}; + \end{tikzpicture} + + \vspace{0.25em} + \begin{tikzpicture}[font=\scriptsize\sffamily, + card/.style={draw=PhantomInk,rounded corners=4pt,minimum width=3.85cm,text width=3.55cm,minimum height=1.4cm,align=center}] + \node[card,fill=PhantomInk,text=white] at (-4.05,0) + {\large$\mathrm{COI}(\pi)=\mathbb{E}[P]-\underline p$\\[-0.05em]\footnotesize pricing power KPI}; + \node[card,fill=PhantomCyan!16,text=PhantomSlate] at (0,0) + {\Large\bfseries$-9{,}014$\\[-0.05em]\footnotesize revenue units\\per +0.1 contamination}; + \node[card,fill=PhantomIndigo!12,text=PhantomSlate] at (4.05,0) + {\large$\lim_{N\to\infty}\mathrm{COI}=0$\\[-0.05em]\footnotesize theorem-level pressure}; + \end{tikzpicture} + + \vspace{0.15em} + {\footnotesize\textbf{Implication:} if quote discovery and purchase split, standard session-based pricing overestimates willingness to pay.} + \stagebar{1} +\end{frame} + +\begin{frame}{The thesis answers one chain: mechanism \(\to\) signal \(\to\) control} + \begin{enumerate}[<+->]\setlength{\itemsep}{0.45em} + \item \textbf{Mechanism (SQ2):} independent reconnaissance pushes realizable price toward the order-statistics floor. + \item \textbf{Signal (SQ1):} human and agent sessions are behaviorally separable from trajectories alone. + \item \textbf{Control (SQ3):} the session score feeds a robust pricing learner under contamination uncertainty. + \end{enumerate} + + \vspace{0.35em} + \stagebar{1} +\end{frame} + +\section{Platform Development} + +\begin{frame}{Stage 1: We built a dual-loop platform to observe behavior and price exposure together} + \centering + \begin{tikzpicture}[ + font=\scriptsize\sffamily, + box/.style={draw=PhantomInk,rounded corners=3pt,minimum width=2.5cm,minimum height=0.9cm,align=center}, + arr/.style={-{Stealth[length=2.2mm]},thick,PhantomSlate} + ] + \node[box,fill=PhantomCyan!14] (actors) at (0,1.45) {Humans + Agents}; + \node[box,fill=white] (web) at (2.9,1.45) {Next.js\\storefront}; + \node[box,fill=white] (provider) at (5.8,1.45) {Pricing\\provider}; + \node[box,fill=white] (redis) at (8.7,1.45) {Redis\\serve layer}; + \node[box,fill=PhantomIndigo!10,minimum width=3.1cm] (kafka) at (4.35,-0.15) {Kafka topics\\behavior + price logs}; + \node[box,fill=PhantomCyan!10,minimum width=2.8cm] (airflow) at (8.0,-0.15) {Airflow + worker\\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.35em} + \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} + \stagebar{2} +\end{frame} + +\begin{frame}{Dataset card: compact, labeled, and 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}, + body/.style={anchor=west,text width=6.0cm,align=left,font=\scriptsize} + ] + \node[draw=PhantomInk,rounded corners=5pt,fill=white,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,font=\scriptsize\ttfamily,text=PhantomSlate] at (-3.2,1.22) + {huggingface.co/datasets/velocitatem/whoclickedit}; + + \node[anchor=west,chip,fill=PhantomCyan!12] at (-3.2,0.65) {\textbf{Rows} 3874}; + \node[anchor=west,chip,fill=PhantomCyan!12] at (-1.70,0.65) {\textbf{Cols} 42}; + \node[anchor=west,chip,fill=PhantomCyan!12] at (-0.25,0.65) {\textbf{Sessions} 36}; + \node[anchor=west,chip,fill=PhantomIndigo!12] (humanrows) at (-3.2,0.03) {\textbf{Human rows} 798}; + \node[anchor=west,chip,fill=PhantomIndigo!12] at ([xshift=0.16cm]humanrows.east) {\textbf{Agent rows} 3076}; + + \node[body,text=PhantomSlate] at (-3.2,-0.68) + {Flat schema and explicit actor labels simplify session-aware train/test splits.}; + \node[body,font=\tiny\itshape,text=PhantomSlate!85] at (-3.2,-1.36) + {Kafka provenance is retained for reproducibility and downstream analysis.}; + \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=PhantomInk,text=white] at (0,1.95) + {\Large\bfseries 13 H / 16 A\\[-0.1em]\footnotesize labeled trajectories in thesis cohort}; + \node[stat,fill=PhantomCyan!14,text=PhantomSlate] at (0,0.25) + {\Large\bfseries 45\% / 55\%\\[-0.1em]\footnotesize human/agent trajectory split}; + \node[stat,fill=PhantomIndigo!12,text=PhantomSlate] at (0,-1.45) + {\Large\bfseries 2 streams\\[-0.1em]\footnotesize interaction + price-log records}; + \end{tikzpicture} + \end{columns} + + \vspace{0.1em} + {\footnotesize\textbf{Use in practice:} this card gives immediate cohort context before any modeling step.} + \stagebar{2} +\end{frame} + +\begin{frame}{Experimental design controls goals, not navigation paths} + \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}, + 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,fill=white] (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,fill=white] (logs) at (0,-1.95) {Session logs\\$e=(a,i,t,\mu,\delta)$ + 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} + \stagebar{2} +\end{frame} + +\section{Distinguishability Construction} + +\begin{frame}{Stage 2: A behavior kernel is a compact signature of navigation dynamics} + \begin{columns}[T,onlytextwidth] + \column{0.48\textwidth} + \begin{block}{Definition} + \[ + \hat P(s'\mid s)=\frac{N(s,s')}{\sum_k N(s,k)} + \] + \end{block} + \begin{itemize}[<+->] + \item Build one kernel per session, then prototypes for human and agent cohorts. + \item Compare each incoming session to both prototypes with KL divergence. + \end{itemize} + + \column{0.50\textwidth} + \centering + \begin{tikzpicture}[font=\scriptsize\sffamily] + \node[draw=PhantomInk,rounded corners=3pt,fill=PhantomCyan!12,minimum width=3.9cm,minimum height=0.85cm] (a) at (0,1.4) {page\_view}; + \node[draw=PhantomInk,rounded corners=3pt,fill=white,minimum width=3.9cm,minimum height=0.85cm] (b) at (0,0.25) {view\_item\_page}; + \node[draw=PhantomInk,rounded corners=3pt,fill=PhantomIndigo!12,minimum width=3.9cm,minimum height=0.85cm] (c) at (0,-0.9) {add\_item\_to\_cart}; + \draw[-{Stealth[length=2.2mm]},thick,PhantomSlate] (a) -- node[right,font=\tiny]{0.64} (b); + \draw[-{Stealth[length=2.2mm]},thick,PhantomSlate] (b) -- node[right,font=\tiny]{0.31} (c); + \draw[-{Stealth[length=2.2mm]},thick,PhantomSlate!70] (b.east) .. controls +(1.1,0.5) and +(1.1,-0.5) .. node[right,font=\tiny]{0.52} (b.east); + \node[font=\tiny\itshape,text=PhantomSlate] at (0,-1.7) {Kernel rows encode ``what usually comes next.''}; + \end{tikzpicture} + \end{columns} + \stagebar{3} +\end{frame} + +\begin{frame}{Human and agent kernels are separable in the controlled cohort} + \begin{columns}[T,onlytextwidth] + \column{0.48\textwidth} + \centering + \textbf{Human transition structure}\par\vspace{0.2em} + \includegraphics[width=\linewidth,height=0.46\textheight,keepaspectratio]{mdp_human.pdf} + \column{0.48\textwidth} + \centering + \textbf{Agent transition structure}\par\vspace{0.2em} + \includegraphics[width=\linewidth,height=0.46\textheight,keepaspectratio]{mdp_agent.pdf} + \end{columns} + + \vspace{0.15em} + \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 test} + \end{columns} + \stagebar{3} +\end{frame} + +\begin{frame}{Two divergence scores become one continuous control signal} + \centering + \[ + f(\tau') = P(A\mid\tau') = \sigma\!\left(\frac{\Delta_H-\Delta_A}{T}\right) + \] + + \vspace{0.4em} + \begin{tikzpicture}[font=\scriptsize\sffamily] + \draw[very thick,PhantomSlate] (-4,0) -- (4,0); + \draw[thick,PhantomSlate] (0,-0.16) -- (0,0.16); + \node[anchor=north] at (-4,0) {human-like}; + \node[anchor=north] at (4,0) {agent-like}; + \node[anchor=north] at (0,0) {$\Delta_H-\Delta_A=0$}; + \fill[PhantomCyan!75!black] (-2.2,0) circle (2.2pt); + \fill[PhantomIndigo!75!black] (2.2,0) circle (2.2pt); + \node[anchor=south,text=PhantomCyan!75!black] at (-2.2,0) {low $f(\tau')$}; + \node[anchor=south,text=PhantomIndigo!75!black] at (2.2,0) {high $f(\tau')$}; + \end{tikzpicture} + + \vspace{0.25em} + \begin{itemize}[<+->] + \item Continuous scoring is used to steer contamination-aware pricing. + \item The design target is guidance, not a hard user-level ban decision. + \end{itemize} + \stagebar{3} +\end{frame} + +\section{Distributionally Robust RL} + +\begin{frame}{Stage 3: DR-RL trains against plausible contamination shifts, not one fixed world} + \small + \begin{columns}[T,onlytextwidth] + \column{0.48\textwidth} + \begin{block}{Ideal robust object} + \[ + \mathcal U_\epsilon(\hat P_N)=\{Q: W_p(Q,\hat P_N)\le\epsilon\} + \] + \centering + robust against distribution shift around the empirical demand law + \end{block} + + \column{0.50\textwidth} + \begin{block}{Engine approximation used in experiments} + \[ + \mathcal A_{\epsilon_\alpha}(\alpha_0)=\{\alpha:|\alpha-\alpha_0|\le\epsilon_\alpha\} + \] + \centering + small grid over $\alpha$ \;\textrightarrow\; inner worst-case candidate + \end{block} + \end{columns} + \vspace{0.2em} + \begin{alertblock}{Practical boundary} + In code we solve a local robust loop around $\alpha_0$, not the full continuous Wasserstein adversary. + \end{alertblock} + \stagebar{4} +\end{frame} + +\begin{frame}{Reward composition penalizes leakage while guarding user experience} + \[ + r_t = + {\color{PhantomInk}\underline{R(p_t,\hat Q_t)}} + - {\color{PhantomCyan!95!black}\underline{\lambda\,f(\tau'_t)\,c_{\text{info}}}} + - {\color{PhantomIndigo!95!black}\underline{\eta_{\text{ux}}\,UX(\tau'_t,p_t)}} + \] + + \vspace{0.45em} + \begin{columns}[T,onlytextwidth] + \column{0.32\textwidth} + \centering + \begin{tikzpicture}[font=\scriptsize\sffamily] + \node[ + draw=PhantomInk, + rounded corners=4pt, + fill=PhantomInk!12, + minimum width=0.98\linewidth, + text width=0.88\linewidth, + minimum height=1.28cm, + align=center, + text=PhantomInk + ] {\textbf{Revenue term}\\[-0.08em]keeps market objective explicit}; + \end{tikzpicture} + \column{0.32\textwidth} + \centering + \begin{tikzpicture}[font=\scriptsize\sffamily] + \node[ + draw=PhantomInk, + rounded corners=4pt, + fill=PhantomCyan!16, + minimum width=0.98\linewidth, + text width=0.88\linewidth, + minimum height=1.28cm, + align=center, + text=PhantomCyan!95!black + ] {\textbf{Leakage term}\\[-0.08em]scales with agent-likelihood score}; + \end{tikzpicture} + \column{0.32\textwidth} + \centering + \begin{tikzpicture}[font=\scriptsize\sffamily] + \node[ + draw=PhantomInk, + rounded corners=4pt, + fill=PhantomIndigo!16, + minimum width=0.98\linewidth, + text width=0.88\linewidth, + minimum height=1.28cm, + align=center, + text=PhantomIndigo!95!black + ] {\textbf{UX term}\\[-0.08em]discourages unstable pricing behavior}; + \end{tikzpicture} + \end{columns} + + \vspace{0.25em} + \begin{itemize}[<+->] + \item Baseline experiments use a query-tax leakage surrogate for tractability. + \item Supra-competitive anchor penalties are tracked as an additional safety rail. + \end{itemize} + \stagebar{4} +\end{frame} + +\begin{frame}{Computationally, wide sweeps are feasible only with aggressive optimization} + \begin{columns}[T,onlytextwidth] + \column{0.47\textwidth} + \centering + {\Large\(4\times4\times3\times2\times2=\mathbf{192}\)}\\[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{\textasciitilde180 days}{net compute logged in full study} + + \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 lookup bottlenecks replaced with array/JAX-style loops. + \item Throughput gains (8.5$\times$, 19$\times$) made broad sweeps practical. + \end{itemize}} + \end{columns} + \stagebar{4} +\end{frame} + +\section{Results} + +\begin{frame}{Results: contamination hurts revenue; defended policies recover COI} + \begin{columns}[T,onlytextwidth] + \column{0.62\textwidth} + \centering + \includegraphics[width=\linewidth,height=0.60\textheight,keepaspectratio]{final_focus_coi_by_alpha.pdf} + + \column{0.30\textwidth} + \metriccard{-90{,}140}{baseline contamination slope}\\[0.3em] + \metriccard{\textasciitilde3\%}{short-run revenue cost of defense}\\[0.3em] + \metriccard{Regime-dependent}{COI gains strongest at harder settings} + \end{columns} + \stagebar{5} +\end{frame} + +\section{Conclusions} + +\begin{frame}{Yes, with boundaries: we can defend margin integrity 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.35em} + \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} + \stagebar{6} +\end{frame} + +\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 relying on device fingerprinting. + \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} + \stagebar{6} +\end{frame} + +\begin{frame}[plain] + \centering + \vfill + {\LARGE\bfseries Thank you} + \vspace{0.8em} + + {\large Questions and discussion} + + \vfill + {\footnotesize\color{PhantomSlate!80}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..abc8ded --- /dev/null +++ b/paper/defense/defense_appendix.tex @@ -0,0 +1,322 @@ +% 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} \\ + 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} \\ + \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; true demand \(d(p;\theta)\) stays latent. Pricing reacts to \(\hat{q}\), so agent-shaped behavior poisons 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: Stackelberg timing (words)} + \begin{itemize} + \item Leader: platform sets price vector given current state and policy. + \item Follower: demand proxy updates from simulated trajectories drawn from \(\mathcal{G}(\alpha)\) and kernels \((\hat{T}_H,\hat{T}_A)\). + \item \textbf{Limbo} buffer stores alternating moves for a clean game history; relaxing strict alternation is listed future work. + \end{itemize} +\end{frame} + +\begin{frame}{Appendix: three layers of evidence} + \footnotesize + \begin{description} + \item[Theorem 1] Formal COI erosion under independence and fixed-offer assumptions. + \item[Simulator] Dynamic, adaptive pricing and contamination sweeps (different status). + \item[Implementation] Local-$\alpha$ robust training; spirit of DRO without claiming a full numerical Wasserstein solver. + \end{description} +\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"