Compare commits

..

77 Commits

Author SHA1 Message Date
43bcad2a98 arxiv mirror 2026-03-10 15:03:57 +01:00
8404a88ef1 fix: logging into benchmark of wandb 2026-03-10 14:54:44 +01:00
1c2935dc87 feat: reference of blog series 2026-03-10 14:54:35 +01:00
14aae3dc9a improving understandable aspects of the abstract 2026-03-10 14:52:17 +01:00
a9f1e19488 feat: docs 2026-03-10 14:23:49 +01:00
4c7d911043 feature: telemetry logging 2026-03-10 14:23:17 +01:00
be03b2d4d5 updating docs and paper 2026-03-10 14:11:41 +01:00
ee32ab7d1d chore: change colors 2026-03-09 22:47:51 +01:00
969ef4c363 feat: working full flow of diff scenes 2026-03-09 20:59:22 +01:00
3cc2dc40d5 initaial defense scenes 2026-03-09 13:52:11 +01:00
529d00cb80 chore: proper naming of authoers 2026-03-09 12:18:34 +01:00
b62d29cfaf updating workflow 2026-03-09 11:45:49 +01:00
77f45ed0b3 initial results :/ 2026-03-09 11:37:22 +01:00
73a1dafc6e first meaningful runs 2026-03-08 21:37:13 +01:00
4c658a93a7 cleaning up jax bs 2026-03-08 19:15:58 +01:00
73246d7dd8 refactoring training spc setup and benchmarking 2026-03-08 18:30:53 +01:00
9fafb26ec8 mirror cloudflare sync 2026-03-08 16:04:49 +01:00
b7b871f9aa fixing line break 2026-03-08 15:52:30 +01:00
840a13ca4a fixing root file passsing 2026-03-08 15:46:21 +01:00
04fa7cbab5 workflow fix 2026-03-08 15:24:56 +01:00
ec7486ee85 genpop workflow 2026-03-08 15:22:09 +01:00
916e72f0ff helper scripts 2026-03-08 14:36:24 +01:00
69b2d5aceb adding task details 2026-03-08 14:32:16 +01:00
28dbcacd95 updating computation power graph 2026-03-08 14:22:54 +01:00
17c128cbc0 implementing nx management for the framework 2026-03-08 13:54:16 +01:00
cc24ac72f7 changed to new test method for singificance 2026-03-08 13:53:31 +01:00
4b89b64674 monestary updates 2026-03-08 13:27:17 +01:00
ec880db444 chore: cleaning the code 2026-02-28 23:38:38 +01:00
803e3a2972 chore: cleaning some code 2026-02-28 23:30:16 +01:00
233ce3be34 class separaiblity significance 2026-02-28 21:38:46 +01:00
Daniel Alves Rösel
8f20359c8c Merge pull request #53 from velocitatem/cleanup
Cleanup
2026-02-28 14:15:24 +01:00
56585b3de8 cleaning path for intergations 2026-02-28 14:11:39 +01:00
5444a4ea13 catchup: rogue scripts 2026-02-27 12:45:46 +01:00
e8a9716f69 hotfix of machines 2026-02-27 11:59:08 +01:00
e50d643fbf feat: training update 2026-02-27 09:33:04 +01:00
Daniel Alves Rösel
dac1e58a0d Merge pull request #52 from velocitatem/enriching-engine-simulation
Enriching engine simulation
2026-02-25 09:21:26 +01:00
4667a1678f chore minor paper edits 2026-02-25 09:16:00 +01:00
a4b7b5b4b2 improements of the methodology for now almost ready tosubmit 2026-02-19 18:28:40 +01:00
1a9901f118 refactored training approaches 2026-02-19 18:23:08 +01:00
5912062dc0 new trainer image 2026-02-19 13:03:25 +01:00
843564eeb0 TPU startup scripts 2026-02-19 13:03:03 +01:00
9acc998cc9 fixing models for gcp 2026-02-17 16:54:55 +01:00
802f31b4a1 adding naive jax and libraries and make adjustments 2026-02-17 14:48:18 +01:00
66c4a0cd1d chore: fix chips used 2026-02-17 14:46:43 +01:00
244af9ac09 citing compute 2026-02-17 14:46:34 +01:00
76c31a2abd citing marc and 2026-02-17 09:40:20 +01:00
64ee7e6d9b forcing light mode 2026-02-16 11:30:18 +01:00
1e04a928aa migrated new banner 2026-02-15 17:31:31 +01:00
9b133cddfd introduce penalized sessions to episodes 2026-02-15 17:15:25 +01:00
ded7290935 hidef banner rendering 2026-02-15 17:12:12 +01:00
8e4dd59f90 banner rendering 2026-02-15 17:10:16 +01:00
024f6d4132 banner addition 2026-02-15 17:10:13 +01:00
2b47c3499a chore: fixing discretization of actions 2026-02-15 15:45:46 +01:00
ef1d1f6557 fixing assumption definition 2026-02-14 21:54:42 +01:00
d7657db287 reintroducing our note :) 2026-02-14 21:49:40 +01:00
e8229ac313 updating methodology with better refelction 2026-02-14 15:20:38 +01:00
bc6c481d03 minor refactors to codebase to implement DRO 2026-02-14 14:53:30 +01:00
895eea5674 imporving methodology and adding onto it 2026-02-14 14:28:18 +01:00
fba2a9739e updating paper details 2026-02-14 13:13:00 +01:00
d1aa13360f cleaning refactors 2026-02-13 21:03:02 +01:00
f6f9729424 improving expression of ideas from dump 2026-02-10 18:12:49 +01:00
29a13340b9 hotfix: updating pricing provider to better read data 2026-02-06 12:01:12 +01:00
e22286371f feat: proportiona lrevenu 2026-02-06 11:54:23 +01:00
e44feb7da0 updaing coi definition 2026-02-05 12:47:13 +01:00
ebd2378859 yapping 2026-02-05 12:28:26 +01:00
c4d82b2ecc rescaling the graph 2026-02-02 16:55:06 +01:00
a9e2e7cbf3 improving on the methodlology 2026-02-02 16:52:50 +01:00
e0b074161b fix: typo 2026-02-02 12:08:24 +01:00
08c0afb55a chore: add chart of supra competive pricing 2026-02-02 12:03:30 +01:00
c4fd1352c9 naoice COI implementation 2026-02-02 11:18:37 +01:00
4abef97bf7 chore: adding simulation logging with wandb 2026-01-31 16:21:10 +01:00
33cb0d7e95 feature: refactored demand splitting and implementation 2026-01-31 12:56:48 +01:00
e8ef850089 feat: introduced simple COI proxy 2026-01-31 12:06:48 +01:00
e7cb48e9cd chore: updating paper 2026-01-31 10:47:12 +01:00
Daniel Alves Rösel
dba8f3fafa Merge pull request #44 from velocitatem/agent-behavior-loader-developemen
Agent behavior loader developement + rl loop definition and e2e tests.
2026-01-31 10:21:54 +01:00
Daniel Alves Rösel
9843c5deab Merge pull request #51 from velocitatem/feat-strong-learning-implementation-with-data-contamination
Feat strong learning implementation with data contamination
2026-01-31 10:15:09 +01:00
Daniel Alves Rösel
b5f19e04b7 Paper lit review (#45)
* chore: updating apa citation and fixing citation in-text and parent

* fixing in lit review

* adjusting citations and improving schema

* chore: fixed formating and adjusting other components

* refined abstract

* one page fitting

* constrainative proposals

* fix: syntax of transtion probs

* refined lit review and soruces

* research Objectives

* adding logo graphics

* chore: fixing citation completeness

* updating with newly built algoerith

* lit review document setup
2026-01-26 13:04:32 +01:00
135 changed files with 20187 additions and 645 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
.git
.venv
.venv-tpu
**/__pycache__
**/*.pyc
**/*.pyo
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
**/.ipynb_checkpoints
wandb
build
paper/build
paper/build-cais
node_modules
**/node_modules
*.egg-info

24
.env.sweep.example Normal file
View File

@@ -0,0 +1,24 @@
# Copy this file to .env.sweep and fill in values.
# Required for wandb runs and sweep agent workers.
WANDB_API_KEY=
WANDB_ENTITY=
WANDB_PROJECT=capstone
# Required for private repo bootstrap workers.
GITHUB_TOKEN=
# Optional defaults for bootstrap mode.
# REPO_URL=https://github.com/org/repo.git
# BRANCH=main
# WORKDIR=$HOME/PHANTOM-agent
# SWEEP_ID=entity/project/id
# AGENT_COUNT=0
# AGENT_LOOP=1
# RETRY_SECONDS=20
# Optional local benchmark defaults.
# LOCAL_BENCHMARK_ARGS=--tiers static,surge,linear,qtable,ppo --alpha-values 0.0,0.3 --episodes 3 --total-timesteps 3000 --max-steps 40 --device cpu
# SIMPLE_BENCHMARK_ARGS=--tiers qtable,ppo,dqn,a2c --alpha-values 0.0,0.15,0.3,0.45,0.6 --episodes 8 --total-timesteps 8000 --max-steps 40 --device cpu
# PHANTOM_BENCHMARK_COMPARE_ROBUST=1
# BENCHMARK_AGENT_ARGS=--tiers static,surge,linear,qtable,ppo --alpha-values 0.0,0.3,0.6 --episodes 5

View File

@@ -12,32 +12,92 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
steps:
- uses: actions/checkout@v4
- name: Compile LaTeX document
- name: Prepare appendix code snapshot
run: bash paper/concat_code.sh
- name: Generate mirrors with Codex
if: ${{ env.OPENAI_API_KEY != '' }}
uses: openai/codex-action@v1
with:
openai-api-key: ${{ env.OPENAI_API_KEY }}
sandbox: workspace-write
safety-strategy: drop-sudo
working-directory: .
prompt: |
Read and follow the mirror instructions in `paper/src/mirrors/genpop/INSTRUCTIONS.md`.
Source chapters are in `paper/src/chapters/`:
- 01-intro.tex
- 02-literature-review.tex
- 03-methodology.tex
- 04-results.tex
- 05-discussion.tex
- 06-conclusion.tex
Update `paper/src/mirrors/genpop/*.tex` so they mirror the thesis for a general audience according to the instruction file.
Keep LaTeX valid and preserve citation commands and section order.
Then create or update `paper/src/main-mirror-genpop.tex` by using `paper/src/main.tex` as the base and replacing chapter inputs from `chapters/...` to `mirrors/genpop/...`.
Do not change any other project files.
- name: Compute LaTeX roots
id: roots
run: |
{
echo "root_files<<EOF"
echo "main.tex"
for file in paper/src/main-mirror-*.tex; do
if [ -f "$file" ]; then
basename "$file"
fi
done
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "Compiling roots:"
echo "main.tex"
for file in paper/src/main-mirror-*.tex; do
if [ -f "$file" ]; then
basename "$file"
fi
done
- name: Compile LaTeX documents
uses: xu-cheng/latex-action@v3
with:
root_file: main.tex
root_file: ${{ steps.roots.outputs.root_files }}
working_directory: paper/src
args: -pdf -f -interaction=nonstopmode -file-line-error -outdir=../build
pre_compile: bash ../concat_code.sh
- name: Upload PDF
args: -pdf -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build
- name: Upload PDF artifacts
uses: actions/upload-artifact@v4
with:
name: thesis-pdf
path: paper/build/main.pdf
path: |
paper/build/main.pdf
paper/build/main-mirror-*.pdf
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to Cloudflare R2
if: ${{ env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' && env.R2_ENDPOINT != '' && env.R2_BUCKET_NAME != '' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ env.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ env.R2_SECRET_ACCESS_KEY }}
AWS_ENDPOINT_URL: ${{ env.R2_ENDPOINT }}
DATE: ${{ steps.date.outputs.date }}
BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
BUCKET_NAME: ${{ env.R2_BUCKET_NAME }}
run: |
pip install boto3
python3 << 'EOF'
@@ -71,4 +131,49 @@ jobs:
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded thesis-latest.pdf")
# upload mirror versions (if generated)
build_dir = 'paper/build'
for filename in os.listdir(build_dir):
if not filename.startswith('main-mirror-') or not filename.endswith('.pdf'):
continue
mirror_name = filename[len('main-mirror-'):-4]
source_path = os.path.join(build_dir, filename)
dated_mirror = f"thesis-{mirror_name}-{date}.pdf"
latest_mirror = f"thesis-{mirror_name}-latest.pdf"
namespaced_dated = f"mirrors/{mirror_name}/thesis-{date}.pdf"
namespaced_latest = f"mirrors/{mirror_name}/thesis-latest.pdf"
s3.upload_file(
source_path,
bucket,
dated_mirror,
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded {dated_mirror}")
s3.upload_file(
source_path,
bucket,
latest_mirror,
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded {latest_mirror}")
s3.upload_file(
source_path,
bucket,
namespaced_dated,
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded {namespaced_dated}")
s3.upload_file(
source_path,
bucket,
namespaced_latest,
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded {namespaced_latest}")
EOF

65
.gitignore vendored
View File

@@ -1,21 +1,54 @@
# environment and secrets
**/.env
.env.*
!.env.*.example
**/.venv
# python build/cache artifacts
**/__pycache__
phantom.egg-info/
*.egg-info/
# notebook artifacts
**/.ipynb_checkpoints/
**/.virtual_documents/
# editor/tool state
**/.pdf-view-restore
.nextstep
.ignore-gitlogue
.cloudflare
.nx/
node_modules/
dist/
# generated svg/graphics
**/session_*.svg
**/*graph.svg
**/auto/*.el
# misc generated
*.old
**/package-lock.json
**/*.parquet
**/_build/
# paper build artifacts
paper/src/bib/auto
=======
**/_build/
paper/src/auto/*
paper/src/bib/auto
paper/template/*
paper/build-cais/
paper/defense/manim/media/
paper/defense/manim/.manim/
paper/src/main.pdf
paper/src/main-blx.bib
paper/src/svg-inkscape/
paper/variations/
paper/src/graphics/test_*.png
thesis-latest.pdf
# experiment run artifacts and logs
docs/goals/*.md
PHANTOM.wiki/
experiments/airflow/logs/*
@@ -23,10 +56,36 @@ experiments/airflow/logs/scheduler/
experiments/airflow/logs/dag_processor_manager/
experiments/collected_data/
experiments/agents/collected_data/
tests/e2e/test-results/
tests/e2e/node_modules/**
# rl/sim run outputs
sim/rl/behavior_loader/*.dot
sim/rl/behavior_loader/*.png
sim/rl/behavior_loader/*.svg
sim/rl/behavior_loader/*.pdf
tests/e2e/node_modules/**
sim/rl/runs/
lab/case/thesis/runs*/
sim/case/thesis_simplified/runs*/
# model binaries
engine/models/*.zip
engine/studies/results/*
*.zip
# wandb local state
wandb/
# data directory (large datasets)
data/
# ktem local app data
ktem_app_data/
# generated visualization pdfs
*_mdp_viz.pdf
phantom_env_comparison.png
sim/phantom_env_comparison.png
# web clone
PHANTOM_web/*

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
CLAUDE.md

186
Makefile
View File

@@ -8,90 +8,180 @@ VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
NX := npx nx
SWEEP_ENV_FILE ?= .env.sweep
WANDB_ENTITY ?=
WANDB_PROJECT ?= capstone
SWEEP_ID ?=
LOCAL_TRAIN_ARGS ?= --algo ppo --total-timesteps 50000
LOCAL_BENCHMARK_ARGS ?= --tiers static,surge,linear,qtable,ppo --alpha-values 0.0,0.3 --episodes 3 --total-timesteps 3000 --max-steps 40 --device cpu
SIMPLE_BENCHMARK_ARGS ?= --tiers qtable,ppo,dqn,a2c --alpha-values 0.0,0.15,0.3,0.45,0.6 --episodes 8 --total-timesteps 8000 --max-steps 40 --device cpu
BENCHMARK_AGENT_ARGS ?=
AGENT_COUNT ?= 0
REPO_URL ?=
BRANCH ?= main
WORKDIR ?= $(HOME)/PHANTOM-agent
AGENT_LOOP ?= 1
RETRY_SECONDS ?= 20
TRAIN_IMAGE_REF := us-central1-docker.pkg.dev/phantom-trc/phantom/phantom-trainer
SWEEP_ENV_LOAD = set -a; [ -f "$(SWEEP_ENV_FILE)" ] && . "$(SWEEP_ENV_FILE)" || true; set +a
.DEFAULT_GOAL := help
.PHONY: help
help:
@echo "pdf.build pdf.watch pdf.clean | test.backend test.e2e test.all | web.dev | install | stats.lines"
@echo "pdf.build pdf.watch pdf.clean pdf.genpop pdf.genpop.watch pdf.arxiv | test.backend test.e2e test.all | web.dev | install | train | benchmark | benchmark.simple | benchmark.agent | train.agent | train.bootstrap | stats.lines"
@echo "backend.server backend.provider backend.worker | platform.up platform.down platform.logs | docker.train.publish"
@echo ""
@echo "Build general public version:"
@echo " make pdf.genpop"
@echo ""
@echo "Local wandb run:"
@echo " make train LOCAL_TRAIN_ARGS='--algo ppo --total-timesteps 50000'"
@echo ""
@echo "Local benchmark run:"
@echo " make benchmark LOCAL_BENCHMARK_ARGS='--tiers static,surge,linear --alpha-values 0.0,0.3 --episodes 3 --no-wandb'"
@echo ""
@echo "Simple benchmark run (.env.sweep defaults, robust+no_robust compare by default):"
@echo " make benchmark.simple"
@echo ""
@echo "Local sweep agent from this repo:"
@echo " make train.agent SWEEP_ID=entity/project/id AGENT_COUNT=5"
@echo ""
@echo "Bootstrap private repo worker from anywhere:"
@echo " make train.bootstrap REPO_URL=https://github.com/org/repo.git BRANCH=main SWEEP_ID=entity/project/id"
@echo ""
@echo "Config source: $(SWEEP_ENV_FILE) (auto-loaded)"
$(BUILDDIR):
mkdir -p paper/$(BUILDDIR)
.PHONY: pdf.build
pdf.build: $(BUILDDIR)
@bash paper/concat_code.sh
@cd $(SRCDIR) && \
$(LATEXMK) -pdf -jobname=$(JOBNAME) -f \
-interaction=nonstopmode -file-line-error \
-r ../.latexmkrc \
-outdir=../$(BUILDDIR) $(TEX)
pdf.build:
@$(NX) run paper:build
.PHONY: pdf.watch
pdf.watch: $(BUILDDIR)
@cd $(SRCDIR) && \
$(LATEXMK) -pvc -pdf -jobname=$(JOBNAME) -f \
-interaction=nonstopmode -file-line-error \
-r ../.latexmkrc \
-outdir=../$(BUILDDIR) $(TEX)
pdf.watch:
@$(NX) run paper:watch
.PHONY: pdf.clean
pdf.clean:
@cd $(SRCDIR) && \
$(LATEXMK) -C -jobname=$(JOBNAME) -outdir=../$(BUILDDIR) || true
rm -rf paper/$(BUILDDIR)/*
@$(NX) run paper:clean
.PHONY: pdf.genpop
pdf.genpop:
@bash scripts/nx_paper.sh build-genpop
.PHONY: pdf.genpop.watch
pdf.genpop.watch:
@bash scripts/nx_paper.sh watch-genpop
.PHONY: pdf.arxiv
pdf.arxiv:
@bash scripts/nx_paper.sh build-arxiv
.PHONY: test.backend
test.backend: $(VENV)
$(PYTEST) -v
test.backend:
@$(NX) run research:test
.PHONY: test.e2e
test.e2e:
@cd tests/e2e && npm install
@cd tests/e2e && npx playwright install chromium
@test -f tests/e2e/.env || cp tests/e2e/.env.example tests/e2e/.env
@timeout 30 bash -c 'until curl -sf http://localhost:5000/health > /dev/null 2>&1; do sleep 1; done' || (echo "Backend not ready" && exit 1)
@timeout 30 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do sleep 1; done' || (echo "Web app not ready" && exit 1)
@timeout 30 bash -c 'until curl -sf http://localhost:8085/health > /dev/null 2>&1; do sleep 1; done' || (echo "Airflow not ready" && exit 1)
@cd tests/e2e && npm test
@$(NX) run e2e:test
.PHONY: test.all
test.all: test.backend test.e2e
test.all:
@$(NX) run-many -t test --projects=research,e2e --parallel=1
.PHONY: web.dev
web.dev:
@cd web && npm install && npm run dev
@$(NX) run web:dev
$(VENV):
python3 -m venv $(VENV)
$(PIP) install --upgrade pip
.PHONY: install
install: $(VENV)
$(PIP) install -r requirements.txt
install:
@$(NX) run research:install
.PHONY: train
train:
@WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" SWEEP_ENV_FILE="$(SWEEP_ENV_FILE)" LOCAL_TRAIN_ARGS="$(LOCAL_TRAIN_ARGS)" $(NX) run research:train
.PHONY: benchmark
benchmark:
@WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" SWEEP_ENV_FILE="$(SWEEP_ENV_FILE)" LOCAL_BENCHMARK_ARGS="$(LOCAL_BENCHMARK_ARGS)" $(NX) run research:benchmark
.PHONY: benchmark.simple
benchmark.simple:
@WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" SWEEP_ENV_FILE="$(SWEEP_ENV_FILE)" SIMPLE_BENCHMARK_ARGS="$(SIMPLE_BENCHMARK_ARGS)" PHANTOM_BENCHMARK_COMPARE_ROBUST="$(PHANTOM_BENCHMARK_COMPARE_ROBUST)" $(NX) run research:benchmark-simple
.PHONY: benchmark.agent
benchmark.agent:
@WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" SWEEP_ENV_FILE="$(SWEEP_ENV_FILE)" SWEEP_ID="$(SWEEP_ID)" AGENT_COUNT="$(AGENT_COUNT)" BENCHMARK_AGENT_ARGS="$(BENCHMARK_AGENT_ARGS)" $(NX) run research:benchmark-agent
.PHONY: train.agent
train.agent:
@WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" SWEEP_ENV_FILE="$(SWEEP_ENV_FILE)" SWEEP_ID="$(SWEEP_ID)" AGENT_COUNT="$(AGENT_COUNT)" $(NX) run research:train-agent
.PHONY: train.bootstrap
train.bootstrap:
@WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" SWEEP_ENV_FILE="$(SWEEP_ENV_FILE)" REPO_URL="$(REPO_URL)" BRANCH="$(BRANCH)" WORKDIR="$(WORKDIR)" SWEEP_ID="$(SWEEP_ID)" AGENT_COUNT="$(AGENT_COUNT)" AGENT_LOOP="$(AGENT_LOOP)" RETRY_SECONDS="$(RETRY_SECONDS)" $(NX) run research:train-bootstrap
.PHONY: stats.lines
stats.lines:
@find . \( -path '*/node_modules' -o -path '*/.venv' -o -path '*/venv' \) -prune -o \
\( -name "*.ts" -o -name "*.py" \) -type f -print0 | xargs -0 cat | wc -l
@$(NX) run research:stats
.PHONY wordcount
.PHONY: wordcount
wordcount:
@echo "Counting words in main text (excluding appendix)..."
@texcount -nosub -total -sum -1 \
$(SRCDIR)/chapters/01-intro.tex \
$(SRCDIR)/chapters/02-literature-review.tex \
$(SRCDIR)/chapters/03-methodology.tex \
$(SRCDIR)/chapters/04-results.tex \
$(SRCDIR)/chapters/05-discussion.tex \
$(SRCDIR)/chapters/06-conclusion.tex
@$(NX) run paper:wordcount
.PHONY: docker.train.publish
docker.train.publish:
@TRAIN_IMAGE_REF="$(TRAIN_IMAGE_REF)" $(NX) run research:docker-train-publish
.PHONY: backend.server backend.provider backend.worker platform.up platform.down platform.logs
backend.server:
@$(NX) run backend-server:dev
backend.provider:
@$(NX) run pricing-provider:dev
backend.worker:
@$(NX) run backend-worker:dev
platform.up:
@$(NX) run platform:up
platform.down:
@$(NX) run platform:down
platform.logs:
@$(NX) run platform:logs
.PHONY: pdf clean watch run.webapp test count-lines all
pdf: pdf.build
clean: pdf.clean
watch: pdf.watch
run.webapp: web.dev
test: test.backend
count-lines: stats.lines
all: pdf.build
pdf:
@$(NX) run paper:build
clean:
@$(NX) run paper:clean
watch:
@$(NX) run paper:watch
run.webapp:
@$(NX) run web:dev
test:
@$(NX) run research:test
count-lines:
@$(NX) run research:stats
all:
@$(NX) run paper:build

6
TPUS/README.md Normal file
View File

@@ -0,0 +1,6 @@
64 spot Cloud TPU v6e chips in zone europe-west4-a
32 spot Cloud TPU v4 chips in zone us-central2-b
64 spot Cloud TPU v5e chips in zone us-central1-a
64 spot Cloud TPU v6e chips in zone us-east1-d
32 on-demand Cloud TPU v4 chips in zone us-central2-b
64 spot Cloud TPU v5e chips in zone europe-west4-b

View File

@@ -0,0 +1,22 @@
# 32 spot Cloud TPU v4 chips in zone us-central2-b
export PROJECT_ID=phantom-trc
export QR_NAME=TPUv4s32spotUC2B
export TPU_NAME=tpu-v4-32-uc2b-spot
export ZONE=us-central2-b
export ACCELERATOR_TYPE=v4-32
export RUNTIME_VERSION=v2-alpha-tpuv4
gcloud compute tpus tpu-vm create ${TPU_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--accelerator-type=${ACCELERATOR_TYPE} \
--version=${RUNTIME_VERSION} \
--spot \
|| \
gcloud compute tpus queued-resources create ${QR_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--node-id=${TPU_NAME} \
--accelerator-type=${ACCELERATOR_TYPE} \
--runtime-version=${RUNTIME_VERSION} \
--spot

13
TPUS/v4_uscentral2b.sh Normal file
View File

@@ -0,0 +1,13 @@
# 32 on-demand Cloud TPU v4 chips in zone us-central2-b
export PROJECT_ID=phantom-trc
export QR_NAME=TPUlong
export ZONE=us-central2-b
export ACCELERATOR_TYPE=v4-32
export RUNTIME_VERSION=v2-alpha-tpuv4
#gcloud compute tpus tpu-vm create ${TPU_NAME} --zone=${ZONE} --project=${PROJECT_ID} --accelerator-type=${ACCELERATOR_TYPE} --version=${RUNTIME_VERSION}
gcloud compute tpus queued-resources create ${QR_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--node-id=${TPU_NAME} \
--accelerator-type=${ACCELERATOR_TYPE} \
--runtime-version=${RUNTIME_VERSION}

View File

@@ -0,0 +1,22 @@
# 64 spot Cloud TPU v5e chips in zone europe-west4-b
export PROJECT_ID=phantom-trc
export QR_NAME=TPUv5e64spotEW4B
export TPU_NAME=tpu-v5e-64-ew4b
export ZONE=europe-west4-b
export ACCELERATOR_TYPE=v5e-64
export RUNTIME_VERSION=v2-alpha-tpuv5-lite
gcloud compute tpus tpu-vm create ${TPU_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--accelerator-type=${ACCELERATOR_TYPE} \
--version=${RUNTIME_VERSION} \
--spot \
|| \
gcloud compute tpus queued-resources create ${QR_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--node-id=${TPU_NAME} \
--accelerator-type=${ACCELERATOR_TYPE} \
--runtime-version=${RUNTIME_VERSION} \
--spot

View File

@@ -0,0 +1,22 @@
# 64 spot Cloud TPU v5e chips in zone us-central1-a
export PROJECT_ID=phantom-trc
export QR_NAME=TPUv5e64spotUC1A
export TPU_NAME=tpu-v5e-64-uc1a
export ZONE=us-central1-a
export ACCELERATOR_TYPE=v5e-64
export RUNTIME_VERSION=v2-alpha-tpuv5-lite
gcloud compute tpus tpu-vm create ${TPU_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--accelerator-type=${ACCELERATOR_TYPE} \
--version=${RUNTIME_VERSION} \
--spot \
|| \
gcloud compute tpus queued-resources create ${QR_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--node-id=${TPU_NAME} \
--accelerator-type=${ACCELERATOR_TYPE} \
--runtime-version=${RUNTIME_VERSION} \
--spot

View File

@@ -0,0 +1,22 @@
# 64 spot Cloud TPU v6e chips in zone europe-west4-a
export PROJECT_ID=phantom-trc
export QR_NAME=TPUv6e64spotEW4A
export TPU_NAME=tpu-v6e-64-ew4a
export ZONE=europe-west4-a
export ACCELERATOR_TYPE=v6e-64
export RUNTIME_VERSION=v2-alpha-tpuv6e
gcloud compute tpus tpu-vm create ${TPU_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--accelerator-type=${ACCELERATOR_TYPE} \
--version=${RUNTIME_VERSION} \
--spot \
|| \
gcloud compute tpus queued-resources create ${QR_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--node-id=${TPU_NAME} \
--accelerator-type=${ACCELERATOR_TYPE} \
--runtime-version=${RUNTIME_VERSION} \
--spot

View File

@@ -0,0 +1,22 @@
# 64 spot Cloud TPU v6e chips in zone us-east1-d
export PROJECT_ID=phantom-trc
export QR_NAME=TPUv6e64spotUE1D
export TPU_NAME=tpu-v6e-64-ue1d
export ZONE=us-east1-d
export ACCELERATOR_TYPE=v6e-64
export RUNTIME_VERSION=v2-alpha-tpuv6e
gcloud compute tpus tpu-vm create ${TPU_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--accelerator-type=${ACCELERATOR_TYPE} \
--version=${RUNTIME_VERSION} \
--spot \
|| \
gcloud compute tpus queued-resources create ${QR_NAME} \
--project=${PROJECT_ID} \
--zone=${ZONE} \
--node-id=${TPU_NAME} \
--accelerator-type=${ACCELERATOR_TYPE} \
--runtime-version=${RUNTIME_VERSION} \
--spot

33
backend/project.json Normal file
View File

@@ -0,0 +1,33 @@
{
"$schema": "../node_modules/nx/schemas/project-schema.json",
"name": "platform",
"projectType": "application",
"sourceRoot": "backend",
"targets": {
"up": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose up -d",
"cwd": "."
}
},
"down": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose down",
"cwd": "."
}
},
"logs": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose logs --tail=100 -f",
"cwd": "."
}
}
},
"tags": [
"scope:platform",
"type:infra"
]
}

View File

@@ -0,0 +1,39 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "pricing-provider",
"projectType": "application",
"sourceRoot": "backend/provider",
"targets": {
"install": {
"executor": "nx:run-commands",
"options": {
"command": "bash -lc '[ -x ../../.venv/bin/python ] || python3 -m venv ../../.venv; ../../.venv/bin/python -m ensurepip --upgrade; ../../.venv/bin/python -m pip install -r requirements.txt'",
"cwd": "backend/provider"
}
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "../../.venv/bin/uvicorn app:app --host 0.0.0.0 --port ${PROVIDER_PORT:-5001} --reload",
"cwd": "backend/provider"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "../../.venv/bin/uvicorn app:app --host 0.0.0.0 --port ${PROVIDER_PORT:-5001}",
"cwd": "backend/provider"
}
}
},
"tags": [
"scope:backend",
"type:provider"
]
}

View File

@@ -0,0 +1,39 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "backend-server",
"projectType": "application",
"sourceRoot": "backend/server",
"targets": {
"install": {
"executor": "nx:run-commands",
"options": {
"command": "bash -lc '[ -x ../../.venv/bin/python ] || python3 -m venv ../../.venv; ../../.venv/bin/python -m ensurepip --upgrade; ../../.venv/bin/python -m pip install -r requirements.txt'",
"cwd": "backend/server"
}
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "../../.venv/bin/uvicorn app:app --host 0.0.0.0 --port ${BACKEND_PORT:-5000} --reload",
"cwd": "backend/server"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "../../.venv/bin/uvicorn app:app --host 0.0.0.0 --port ${BACKEND_PORT:-5000}",
"cwd": "backend/server"
}
}
},
"tags": [
"scope:backend",
"type:api"
]
}

View File

@@ -1,6 +1,6 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
kafka-python==2.0.2
pydantic==2.5.0
python-dotenv==1.0.0
supabase==2.9.1
fastapi>=0.135,<0.136
uvicorn[standard]>=0.41,<0.42
kafka-python>=2.3,<2.4
pydantic>=2.12,<3
python-dotenv>=1.0,<2
supabase>=2.28,<3

View File

@@ -0,0 +1,39 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "backend-worker",
"projectType": "application",
"sourceRoot": "backend/worker",
"targets": {
"install": {
"executor": "nx:run-commands",
"options": {
"command": "bash -lc '[ -x ../../.venv/bin/python ] || python3 -m venv ../../.venv; ../../.venv/bin/python -m ensurepip --upgrade; ../../.venv/bin/python -m pip install -r requirements.txt'",
"cwd": "backend/worker"
}
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "../../.venv/bin/celery -A main:app worker --loglevel=info",
"cwd": "backend/worker"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "../../.venv/bin/python main.py",
"cwd": "backend/worker"
}
}
},
"tags": [
"scope:backend",
"type:worker"
]
}

View File

@@ -0,0 +1,3 @@
celery>=5.3,<6
python-dotenv>=1.0.0
redis>=5.0.0

15
docker/Trainer.dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# syntax=docker/dockerfile:1.7
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime AS gpu
WORKDIR /app
COPY docker/trainer.requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
COPY --chmod=755 docker/trainer-agent-entrypoint.sh /usr/local/bin/trainer-agent-entrypoint
COPY engine /app/engine
ENV PYTHONPATH=/app
ENTRYPOINT ["/usr/local/bin/trainer-agent-entrypoint"]

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu
if [ -z "${SWEEP_ID:-}" ]; then
echo "SWEEP_ID is required"
exit 1
fi
set -- python -m engine.train --sweep-agent --sweep-id "${SWEEP_ID}"
if [ -n "${PHANTOM_DEFAULT_AGENT_ARGS:-}" ]; then
set -- "$@" ${PHANTOM_DEFAULT_AGENT_ARGS}
fi
if [ -n "${TRAIN_ARGS:-}" ]; then
set -- "$@" ${TRAIN_ARGS}
fi
if [ "${AGENT_COUNT:-0}" != "0" ]; then
set -- "$@" --count "${AGENT_COUNT}"
fi
exec "$@"

View File

@@ -0,0 +1,7 @@
numpy>=1.24.0
pandas>=2.0.0
scipy>=1.11.0
gymnasium>=0.29.0
stable-baselines3>=2.2.0
tensorboard>=2.15.0
wandb>=0.17.0

View File

@@ -17,8 +17,8 @@
<meta property="og:site_name" content="PHANTOM Research">
<meta property="og:title" content="PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms">
<meta property="og:description" content="Developing pricing heuristics to protect e-commerce platforms from systematic exploitation by LLM agents in dynamic pricing environments through behavioral signature detection.">
<meta property="og:url" content="TODO">
<meta property="og:image" content="TODO">
<meta property="og:url" content="https://velocitatem.github.io/PHANTOM/">
<meta property="og:image" content="https://raw.githubusercontent.com/velocitatem/PHANTOM/main/docs/static/images/carousel1.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="PHANTOM Research Preview">
@@ -30,17 +30,12 @@
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<!-- TODO: Replace with your lab/institution Twitter handle -->
<meta name="twitter:site" content="@YOUR_TWITTER_HANDLE">
<!-- TODO: Replace with first author's Twitter handle -->
<meta name="twitter:creator" content="@AUTHOR_TWITTER_HANDLE">
<!-- TODO: Same as paper title above -->
<meta name="twitter:title" content="PAPER_TITLE">
<!-- TODO: Same as description above -->
<meta name="twitter:description" content="BRIEF_DESCRIPTION_OF_YOUR_RESEARCH_CONTRIBUTION_AND_FINDINGS">
<!-- TODO: Same as social preview image above -->
<meta name="twitter:image" content="https://YOUR_DOMAIN.com/static/images/social_preview.png">
<meta name="twitter:image:alt" content="PAPER_TITLE - Research Preview">
<meta name="twitter:site" content="@velocitatem">
<meta name="twitter:creator" content="@velocitatem">
<meta name="twitter:title" content="PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms">
<meta name="twitter:description" content="A thesis project on defending dynamic pricing against LLM-driven reconnaissance and transaction orchestration.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/velocitatem/PHANTOM/main/docs/static/images/carousel1.jpg">
<meta name="twitter:image:alt" content="PHANTOM research visual">
<!-- Academic/Research Specific -->
<meta name="citation_title" content="Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms">
@@ -103,50 +98,42 @@
{
"@context": "https://schema.org",
"@type": "ScholarlyArticle",
"headline": "PAPER_TITLE",
"description": "BRIEF_DESCRIPTION_OF_YOUR_RESEARCH_CONTRIBUTION_AND_FINDINGS",
"headline": "PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms",
"description": "Research on preserving dynamic pricing integrity under LLM-mediated reconnaissance and purchasing behavior.",
"author": [
{
"@type": "Person",
"name": "FIRST_AUTHOR_NAME",
"name": "Daniel Rösel",
"affiliation": {
"@type": "Organization",
"name": "INSTITUTION_NAME"
}
},
{
"@type": "Person",
"name": "SECOND_AUTHOR_NAME",
"affiliation": {
"@type": "Organization",
"name": "INSTITUTION_NAME"
"name": "IE University"
}
}
],
"datePublished": "2024-01-01",
"datePublished": "2025-01-01",
"publisher": {
"@type": "Organization",
"name": "CONFERENCE_OR_JOURNAL_NAME"
"name": "IE University"
},
"url": "https://YOUR_DOMAIN.com/YOUR_PROJECT_PAGE",
"image": "https://YOUR_DOMAIN.com/static/images/social_preview.png",
"keywords": ["KEYWORD1", "KEYWORD2", "KEYWORD3", "machine learning", "computer vision"],
"abstract": "FULL_ABSTRACT_TEXT_HERE",
"citation": "BIBTEX_CITATION_HERE",
"url": "https://velocitatem.github.io/PHANTOM/",
"image": "https://raw.githubusercontent.com/velocitatem/PHANTOM/main/docs/static/images/carousel1.jpg",
"keywords": ["dynamic pricing", "llm agents", "e-commerce", "distributionally robust optimization", "reinforcement learning"],
"abstract": "This thesis formalizes Cost of Information erosion under agentic reconnaissance, learns separable human and agent behavior kernels, and trains contamination-aware robust pricing policies.",
"citation": "Rösel, Daniel. PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms. IE University, 2025.",
"isAccessibleForFree": true,
"license": "https://creativecommons.org/licenses/by/4.0/",
"mainEntity": {
"@type": "WebPage",
"@id": "https://YOUR_DOMAIN.com/YOUR_PROJECT_PAGE"
"@id": "https://velocitatem.github.io/PHANTOM/"
},
"about": [
{
"@type": "Thing",
"name": "RESEARCH_AREA_1"
"name": "Dynamic Pricing"
},
{
"@type": "Thing",
"name": "RESEARCH_AREA_2"
"name": "Agent Behavior Modeling"
}
]
}
@@ -158,8 +145,7 @@
"@context": "https://schema.org",
"@type": "Organization",
"name": "IE University",
"url": "https://www.ie.edu",
"logo": "TODO"
"url": "https://www.ie.edu"
}
</script>
</head>
@@ -173,45 +159,72 @@
<!-- More Works Dropdown -->
<div class="more-works-container">
<button class="more-works-btn" onclick="toggleMoreWorks()" title="View More Works from Our Lab">
<button class="more-works-btn" onclick="toggleMoreWorks()" title="View project links and artifacts">
<i class="fas fa-flask"></i>
More Works
Project Links
<i class="fas fa-chevron-down dropdown-arrow"></i>
</button>
<div class="more-works-dropdown" id="moreWorksDropdown">
<div class="dropdown-header">
<h4>More Works from Our Lab</h4>
<h4>Project Links</h4>
<button class="close-btn" onclick="toggleMoreWorks()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="works-list">
<!-- TODO: Replace with your lab's related works -->
<a href="https://arxiv.org/abs/PAPER_ID_1" class="work-item" target="_blank">
<a href="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" class="work-item" target="_blank">
<div class="work-info">
<!-- TODO: Replace with actual paper title -->
<h5>Paper Title 1</h5>
<!-- TODO: Replace with brief description -->
<p>Brief description of the work and its main contribution.</p>
<!-- TODO: Replace with venue and year -->
<span class="work-venue">Conference/Journal 2024</span>
<h5>Thesis PDF</h5>
<p>Latest public build of the full thesis document.</p>
<span class="work-venue">IE University, 2025</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
<!-- TODO: Add more related works or remove extra items -->
<a href="https://arxiv.org/abs/PAPER_ID_2" class="work-item" target="_blank">
<a href="https://github.com/velocitatem/PHANTOM" class="work-item" target="_blank">
<div class="work-info">
<h5>Paper Title 2</h5>
<p>Brief description of the work and its main contribution.</p>
<span class="work-venue">Conference/Journal 2023</span>
<h5>PHANTOM Repository</h5>
<p>Monorepo with paper source, platform code, and experiments.</p>
<span class="work-venue">Open Source</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
<a href="https://arxiv.org/abs/PAPER_ID_3" class="work-item" target="_blank">
<a href="https://github.com/velocitatem/p4p" class="work-item" target="_blank">
<div class="work-info">
<h5>Paper Title 3</h5>
<p>Brief description of the work and its main contribution.</p>
<span class="work-venue">Conference/Journal 2023</span>
<h5>P4P Interaction Layer</h5>
<p>Reusable storefront and logging layer released for replication.</p>
<span class="work-venue">Public Artifact</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
<a href="https://phantom-hotel.vercel.app" class="work-item" target="_blank">
<div class="work-info">
<h5>Hotel Mode Demo</h5>
<p>Public deployment of the hotel-style experiment interface.</p>
<span class="work-venue">Live Demo</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
<a href="https://phantom-airline.vercel.app" class="work-item" target="_blank">
<div class="work-info">
<h5>Airline Mode Demo</h5>
<p>Public deployment of the airline-style experiment interface.</p>
<span class="work-venue">Live Demo</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
<a href="https://blog.alves.world/series/phantom" class="work-item" target="_blank">
<div class="work-info">
<h5>Blog Series</h5>
<p>Behind-the-scenes posts covering thesis process, tooling, and insights.</p>
<span class="work-venue">To Boldly Code</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
<a href="goals/README.md" class="work-item" target="_blank">
<div class="work-info">
<h5>Goal Library</h5>
<p>Task definitions used to assign actor objectives in experiments.</p>
<span class="work-venue">Experiment Design</span>
</div>
<i class="fas fa-external-link-alt"></i>
</a>
@@ -239,6 +252,16 @@
<div class="column has-text-centered">
<div class="publication-links">
<span class="link-block">
<a href="https://blog.alves.world/series/phantom" target="_blank"
class="external-link button is-normal is-rounded is-dark">
<span class="icon">
<i class="fas fa-blog"></i>
</span>
<span>Blog Series</span>
</a>
</span>
<span class="link-block">
<a href="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" target="_blank"
class="external-link button is-normal is-rounded is-dark">
<span class="icon">
@@ -248,14 +271,13 @@
</a>
</span>
<!-- TODO: Add your supplementary material PDF or remove this section -->
<span class="link-block">
<a href="static/pdfs/supplementary_material.pdf" target="_blank"
<a href="goals/goals.csv" target="_blank"
class="external-link button is-normal is-rounded is-dark">
<span class="icon">
<i class="fas fa-file-pdf"></i>
<i class="fas fa-list"></i>
</span>
<span>Supplementary</span>
<span>Goal Set</span>
</a>
</span>
@@ -269,42 +291,43 @@
</a>
</span>
<!-- TODO: Update with your arXiv paper ID -->
<span class="link-block">
<a href="https://arxiv.org/abs/<ARXIV PAPER ID>" target="_blank"
<a href="https://phantom-hotel.vercel.app" target="_blank"
class="external-link button is-normal is-rounded is-dark">
<span class="icon">
<i class="ai ai-arxiv"></i>
<i class="fas fa-globe"></i>
</span>
<span>arXiv</span>
<span>Hotel Demo</span>
</a>
</span>
<span class="link-block">
<a href="https://phantom-airline.vercel.app" target="_blank"
class="external-link button is-normal is-rounded is-dark">
<span class="icon">
<i class="fas fa-plane"></i>
</span>
<span>Airline Demo</span>
</a>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Teaser video-->
<section class="hero teaser">
<div class="container is-max-desktop">
<div class="hero-body">
<!-- TODO: Replace with your teaser video -->
<video poster="" id="tree" autoplay controls muted loop height="100%" preload="metadata">
<!-- TODO: Add your video file path here -->
<source src="static/videos/banner_video.mp4" type="video/mp4">
</video>
<!-- TODO: Replace with your video description -->
<h2 class="subtitle has-text-centered">
Aliquam vitae elit ullamcorper tellus egestas pellentesque. Ut lacus tellus, maximus vel lectus at, placerat pretium mi. Maecenas dignissim tincidunt vestibulum. Sed consequat hendrerit nisl ut maximus.
</h2>
<div class="publication-banner">
<img src="static/images/banner.svg" alt="PHANTOM teaser diagram connecting vulnerability, behavioral signal, and robust control" width="1920" height="1080" decoding="async" style="display:block; width:100%; height:auto;" onerror="this.onerror=null;this.src='static/images/carousel2.jpg';"/>
</div>
</div>
</div>
</section>
<!-- End teaser video -->
<!-- Paper abstract -->
<section class="section hero is-light">
@@ -314,10 +337,10 @@
<h2 class="title is-3">Abstract</h2>
<div class="content has-text-justified">
<p>
This research establishes the following contributions: definition and formalization of non-human transactors in e-commerce platforms, development of a testing-ground for capturing the behavioral essence of these transactors across a large variety of digital systems, construction of a discriminative model to prove separability as a strong learner for downstream mitigation of contamination by non-human entities, translation of such learned separability into existing dynamic pricing machine learning loops, and establishment of a high-level KPI-affecting causal effect and cost-saving framework for the future of internet commerce in the presence of such non-human learners.
When you shop online, prices often change based on how much interest you show — the more you browse, the more the site learns about your intent and may raise prices accordingly. This works because stores assume that a curious, engaged shopper is more likely to buy. But AI assistants are now doing the shopping research on behalf of users: they browse in one session to gather price information and then let the user purchase in a fresh session at the lower, unadjusted price. The store never sees the connection between the two, so it never gets to factor in that genuine intent — and loses the revenue it would have earned.
</p>
<p>
This work develops behavioral signature models using recommendation system techniques to profile session-level interaction, temporal engagement, and cross-session correlation. The AI Agent market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030, raising the question of how these systems should be designed for future robustness and how to maintain a competitive edge in the analytical components of e-commerce platforms.
PHANTOM studies this problem and builds defenses against it. We created a realistic fake store (in hotel and airline modes) where both real people and AI agents were given shopping tasks, and we recorded every click, scroll, and page visit. By comparing how humans and AI agents move through a site, we found clear patterns that tell them apart. We then used those patterns to build a smarter pricing system that can recognize when it is likely talking to an AI scout and adjust its strategy accordingly — protecting the store's margins without making things worse for genuine shoppers.
</p>
</div>
</div>
@@ -326,97 +349,90 @@
</section>
<!-- End paper abstract -->
<section class="section">
<div class="container is-max-desktop">
<div class="content has-text-justified">
<h2 class="title is-3 has-text-centered">Project Scope</h2>
<p>
The current thesis revision extends both theory and implementation. The main research question is how a pricing system can preserve margin integrity when browsing and purchasing are increasingly orchestrated by AI agents.
</p>
<ul>
<li>Formal contribution: a Cost of Information erosion theorem showing why price-query saturation can collapse dynamic pricing power.</li>
<li>System contribution: a hybrid online/offline stack (Next.js storefront, pricing provider, Kafka event streams, Airflow ETL, Redis serving layer).</li>
<li>Modeling contribution: class-specific transition kernels for human and agent behavior, with KL-divergence based separability scores.</li>
<li>Control contribution: a contamination-aware DR-RL pricing policy trained under distributional uncertainty using Wasserstein-style robustness.</li>
</ul>
<p>
Controlled trials currently include balanced human and agent sessions with goal-driven tasks across hotel and airline interfaces. Early separability results are strong (Mann-Whitney U=2.0, p=0.0006), while robust pricing gains remain regime-dependent and are being calibrated in larger sweeps.
</p>
</div>
</div>
</section>
<!-- Image carousel -->
<!--
<section class="hero is-small">
<div class="hero-body">
<div class="container">
<div id="results-carousel" class="carousel results-carousel">
<div class="item">
<!-- TODO: Replace with your research result images -->
<img src="static/images/carousel1.jpg" alt="First research result visualization" loading="lazy"/>
<!-- TODO: Replace with description of this result -->
<h2 class="subtitle has-text-centered">
First image description.
Early simulator traces showing how policy choice can push prices toward aggressive high-end regimes.
</h2>
</div>
<div class="item">
<!-- Your image here -->
<img src="static/images/carousel2.jpg" alt="Second research result visualization" loading="lazy"/>
<h2 class="subtitle has-text-centered">
Second image description.
Human and agent behavior diverge at the transition-kernel level, enabling usable session-level separability.
</h2>
</div>
<div class="item">
<!-- Your image here -->
<img src="static/images/carousel3.jpg" alt="Third research result visualization" loading="lazy"/>
<h2 class="subtitle has-text-centered">
Third image description.
End-to-end architecture linking web interactions, pricing queries, event streams, and model updates.
</h2>
</div>
<div class="item">
<!-- Your image here -->
<img src="static/images/carousel4.jpg" alt="Fourth research result visualization" loading="lazy"/>
<h2 class="subtitle has-text-centered">
Fourth image description.
Contamination-aware evaluation compares robust and non-robust pricing behavior across alpha sweeps.
</h2>
</div>
</div>
</div>
</div>
</section>
-->
<!-- End image carousel -->
<!-- Youtube video -->
<section class="hero is-small is-light">
<div class="hero-body">
<div class="container">
<!-- Paper video. -->
<h2 class="title is-3">Video Presentation</h2>
<div class="columns is-centered has-text-centered">
<div class="column is-four-fifths">
<div class="publication-video">
<!-- TODO: Replace with your YouTube video ID -->
<iframe src="https://www.youtube.com/embed/JkaxUblCGz0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- End youtube video -->
<!-- Video carousel -->
<section class="hero is-small">
<div class="hero-body">
<div class="container">
<h2 class="title is-3">Another Carousel</h2>
<div id="results-carousel" class="carousel results-carousel">
<h2 class="title is-3">Defense Scenes</h2>
<div id="videos-carousel" class="carousel results-carousel">
<div class="item item-video1">
<!-- TODO: Add poster image for better preview -->
<video poster="" id="video1" controls muted loop height="100%" preload="metadata">
<!-- Your video file here -->
<source src="static/videos/carousel1.mp4" type="video/mp4">
<source src="static/videos/COIFirstPrinciplesScene.mp4" type="video/mp4">
</video>
<h2 class="subtitle has-text-centered">COI from first principles.</h2>
</div>
<div class="item item-video2">
<!-- TODO: Add poster image for better preview -->
<video poster="" id="video2" controls muted loop height="100%" preload="metadata">
<!-- Your video file here -->
<source src="static/videos/carousel2.mp4" type="video/mp4">
<source src="static/videos/BehaviorKernelConstructionScene.mp4" type="video/mp4">
</video>
<h2 class="subtitle has-text-centered">Behavioral kernel construction: learning how humans and agents differ.</h2>
</div>
<div class="item item-video3">
<!-- TODO: Add poster image for better preview -->
<video poster="" id="video3" controls muted loop height="100%" preload="metadata">
<!-- Your video file here -->
<source src="static/videos/carousel3.mp4" type="video/mp4">
<source src="static/videos/RobustControlScene.mp4" type="video/mp4">
</video>
<h2 class="subtitle has-text-centered">Distributionally robust control loop.</h2>
</div>
</div>
</div>
@@ -432,10 +448,10 @@
<!-- Paper poster -->
<section class="hero is-small is-light">
<div class="hero-body">
<div class="container">
<h2 class="title">Poster</h2>
<div class="container">
<h2 class="title">Full Thesis</h2>
<iframe src="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" width="100%" height="550">
<iframe title="PHANTOM thesis PDF" src="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" width="100%" height="550">
</iframe>
</div>
@@ -457,7 +473,7 @@
</div>
<pre id="bibtex-code"><code>@thesis{Rosel2025PHANTOM,
title={Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms},
author={R{\"o}sel, Daniel},
author={Rösel, Daniel},
school={IE University},
year={2025},
address={Madrid, Spain},

246
docs/static/images/banner.svg vendored Normal file
View File

@@ -0,0 +1,246 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" width="1920" height="1080" style="background-color: #FAFAFA; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">
<defs>
<!-- Soft Drop Shadow for Panels -->
<filter id="shadow" x="-10%" y="-10%" width="130%" height="130%">
<feDropShadow dx="2" dy="4" stdDeviation="6" flood-color="#000000" flood-opacity="0.06"/>
</filter>
<filter id="light-shadow" x="-5%" y="-5%" width="110%" height="110%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.04"/>
</filter>
<!-- Arrowhead Marker -->
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#888888" />
</marker>
<marker id="arrow-dark" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555555" />
</marker>
</defs>
<!-- COLUMN DIVIDERS -->
<line x1="640" y1="60" x2="640" y2="1020" stroke="#EAEAEA" stroke-width="2" stroke-dasharray="10,10"/>
<line x1="1280" y1="60" x2="1280" y2="1020" stroke="#EAEAEA" stroke-width="2" stroke-dasharray="10,10"/>
<!-- ========================================================= -->
<!-- COLUMN 1: THE THREAT (COI & SATURATION) -->
<!-- ========================================================= -->
<text x="60" y="80" font-family="Georgia, serif" font-size="28" font-weight="bold" fill="#333333">1. The Vulnerability</text>
<line x1="60" y1="100" x2="580" y2="100" stroke="#DDDDDD" stroke-width="2"/>
<!-- Top: COI Bell Curve -->
<g transform="translate(60, 130)">
<text x="0" y="30" font-size="24" font-weight="bold" fill="#444">Cost of Information from First Principles</text>
<text x="0" y="70" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">P ~ π(τ)</text>
<text x="0" y="105" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B"><tspan text-decoration="underline">p</tspan> = reservation price</text>
<text x="0" y="140" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">M = P - <tspan text-decoration="underline">p</tspan></text>
<!-- Bell Curve -->
<path d="M 40 340 C 140 340, 160 160, 260 160 C 360 160, 380 340, 480 340" stroke="#3AB09E" stroke-width="5" fill="none"/>
<line x1="40" y1="340" x2="500" y2="340" stroke="#333" stroke-width="2"/>
<!-- Markers p and E[P] -->
<line x1="150" y1="340" x2="150" y2="160" stroke="#E37862" stroke-width="2" stroke-dasharray="6,4"/>
<text x="150" y="375" font-family="Georgia" font-style="italic" font-size="22" fill="#E37862" text-anchor="middle">p</text>
<line x1="260" y1="340" x2="260" y2="160" stroke="#85B589" stroke-width="2" stroke-dasharray="6,4"/>
<text x="260" y="375" font-family="Georgia" font-style="italic" font-size="22" fill="#85B589" text-anchor="middle">E[P]</text>
<!-- COI Annotation -->
<line x1="150" y1="150" x2="260" y2="150" stroke="#E37862" stroke-width="2" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
<text x="310" y="138" font-size="16" fill="#E37862" text-anchor="middle">average information rent</text>
<text x="310" y="118" font-family="Georgia" font-style="italic" font-size="22" fill="#E37862" font-weight="bold" text-anchor="middle">COI := E[P] - p</text>
</g>
<!-- Bottom: Agent Saturation -->
<g transform="translate(60, 580)">
<text x="0" y="30" font-size="24" font-weight="bold" fill="#444">Why COI Erodes with Agent Saturation</text>
<text x="0" y="75" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">p<tspan font-size="14" dy="5">(1)</tspan><tspan dy="-5"> = min(p</tspan><tspan font-size="14" dy="5">1</tspan><tspan dy="-5">, ..., p</tspan><tspan font-size="14" dy="5">N</tspan><tspan dy="-5">)</tspan></text>
<text x="0" y="115" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">P(p<tspan font-size="14" dy="5">(1)</tspan><tspan dy="-5"> > t) = [1 - F(t)]</tspan><tspan font-size="14" dy="-10">N</tspan></text>
<!-- Erosion Graph -->
<rect x="120" y="150" width="280" height="230" fill="#FFFFFF" filter="url(#shadow)" rx="8"/>
<line x1="140" y1="350" x2="380" y2="350" stroke="#333" stroke-width="2"/>
<line x1="140" y1="350" x2="140" y2="170" stroke="#333" stroke-width="2"/>
<text x="260" y="375" font-size="16" font-style="italic" fill="#555" text-anchor="middle">F(t)</text>
<text x="120" y="260" font-size="16" font-style="italic" fill="#555" text-anchor="middle" transform="rotate(-90 120 260)">[1 - F(t)]^N</text>
<!-- Curves -->
<path d="M 140 170 C 220 250, 300 320, 380 350" stroke="#4EA5D9" stroke-width="3" fill="none"/>
<text x="390" y="220" font-size="16" fill="#4EA5D9" font-weight="bold">N=1</text>
<path d="M 140 170 C 180 260, 240 330, 380 350" stroke="#85B589" stroke-width="3" fill="none"/>
<text x="390" y="250" font-size="16" fill="#85B589" font-weight="bold">N=4</text>
<path d="M 140 170 C 150 290, 180 340, 380 350" stroke="#E37862" stroke-width="3" fill="none"/>
<text x="390" y="280" font-size="16" fill="#E37862" font-weight="bold">N=16</text>
<text x="260" y="420" font-size="20" fill="#555" text-anchor="middle">As independent query count grows,</text>
<text x="260" y="445" font-size="20" fill="#E37862" font-weight="bold" text-anchor="middle">realizable markup collapses.</text>
</g>
<!-- ========================================================= -->
<!-- COLUMN 2: THE BEHAVIORAL SIGNAL -->
<!-- ========================================================= -->
<text x="700" y="80" font-family="Georgia, serif" font-size="28" font-weight="bold" fill="#333333">2. The Behavioral Signals</text>
<line x1="700" y1="100" x2="1220" y2="100" stroke="#DDDDDD" stroke-width="2"/>
<!-- Top: Transition Kernels -->
<g transform="translate(700, 130)">
<text x="0" y="30" font-size="24" font-weight="bold" fill="#444">From Session Paths to Transition Kernels</text>
<text x="0" y="75" font-size="20" fill="#85B589" font-weight="bold">human: start → view → detail → cart → purchase</text>
<text x="0" y="115" font-size="20" fill="#E37862" font-weight="bold">agent: start → view → detail → view → detail</text>
<text x="0" y="170" font-family="Georgia, serif" font-style="italic" font-size="24" fill="#8C7A6B">
P&#770;(s'|s) = <tspan font-size="18" dy="-12">N(s,s')</tspan> / <tspan font-size="18" dy="12">Σ N(s,k)</tspan>
</text>
<!-- Matrix Representation -->
<rect x="0" y="220" width="500" height="180" fill="#FFFFFF" filter="url(#shadow)" rx="8"/>
<text x="125" y="250" font-size="16" fill="#4EA5D9" text-anchor="middle">transition counts N(s,s')</text>
<text x="375" y="250" font-size="16" fill="#85B589" text-anchor="middle">normalized kernel T</text>
<!-- Matrix 1 -->
<g transform="translate(45, 270)">
<rect x="-6" y="-8" width="172" height="128" rx="6" fill="none" stroke="#DDDDDD" stroke-width="1.5"/>
<path d="M 10 0 L 0 0 L 0 110 L 10 110 M 150 0 L 160 0 L 160 110 L 150 110" stroke="#A0A0A0" stroke-width="2.5" fill="none"/>
<text x="80" y="20" font-family="monospace" font-size="14" fill="#555" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 8.00 0.00 0.00</text>
<text x="80" y="50" font-family="monospace" font-size="14" fill="#555" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 2.00 5.00 1.00</text>
<text x="80" y="80" font-family="monospace" font-size="14" fill="#555" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 3.00 2.00 4.00</text>
<text x="80" y="110" font-family="monospace" font-size="14" fill="#555" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 1.00 0.00 6.00</text>
</g>
<!-- Arrow -->
<line x1="225" y1="320" x2="265" y2="320" stroke="#999" stroke-width="3" marker-end="url(#arrow-dark)"/>
<!-- Matrix 2 -->
<g transform="translate(295, 270)">
<rect x="-6" y="-8" width="172" height="128" rx="6" fill="none" stroke="#DDDDDD" stroke-width="1.5"/>
<path d="M 10 0 L 0 0 L 0 110 L 10 110 M 150 0 L 160 0 L 160 110 L 150 110" stroke="#A0A0A0" stroke-width="2.5" fill="none"/>
<text x="80" y="20" font-family="monospace" font-size="14" fill="#333" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 1.00 0.00 0.00</text>
<text x="80" y="50" font-family="monospace" font-size="14" fill="#333" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 0.25 0.62 0.13</text>
<text x="80" y="80" font-family="monospace" font-size="14" fill="#333" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 0.33 0.22 0.45</text>
<text x="80" y="110" font-family="monospace" font-size="14" fill="#333" text-anchor="middle" textLength="142" lengthAdjust="spacingAndGlyphs">0.00 0.14 0.00 0.86</text>
</g>
<text x="250" y="440" font-size="18" fill="#777" text-anchor="middle">Kernel shape is the compact behavioral signature used downstream.</text>
</g>
<!-- Bottom: Separability Distributions -->
<g transform="translate(700, 600)">
<text x="0" y="30" font-size="24" font-weight="bold" fill="#444">Separability into a Control Signal</text>
<text x="0" y="75" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">Δ<tspan font-size="16" dy="5">H</tspan><tspan dy="-5"> = D</tspan><tspan font-size="16" dy="5">KL</tspan><tspan dy="-5">(T&#770;' || T&#772;</tspan><tspan font-size="16" dy="5">H</tspan><tspan dy="-5">)</tspan></text>
<text x="0" y="115" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">Δ<tspan font-size="16" dy="5">A</tspan><tspan dy="-5"> = D</tspan><tspan font-size="16" dy="5">KL</tspan><tspan dy="-5">(T&#770;' || T&#772;</tspan><tspan font-size="16" dy="5">A</tspan><tspan dy="-5">)</tspan></text>
<text x="0" y="155" font-family="Georgia, serif" font-style="italic" font-size="24" fill="#8C7A6B">g = Δ<tspan font-size="16" dy="5">H</tspan><tspan dy="-5"> - Δ</tspan><tspan font-size="16" dy="5">A</tspan></text>
<!-- Curves -->
<g transform="translate(80, 160)">
<line x1="0" y1="200" x2="360" y2="200" stroke="#333" stroke-width="2"/>
<text x="180" y="235" font-family="Georgia, serif" font-style="italic" font-size="22" text-anchor="middle">g = Δ<tspan font-size="16" dy="5">H</tspan><tspan dy="-5"> - Δ</tspan><tspan font-size="16" dy="5">A</tspan></text>
<!-- Human Curve -->
<path d="M 0 200 C 50 200, 80 40, 130 40 C 180 40, 210 200, 260 200" stroke="#4EA5D9" stroke-width="5" fill="none"/>
<text x="70" y="110" font-size="22" fill="#4EA5D9" font-weight="bold">human</text>
<!-- Agent Curve -->
<path d="M 100 200 C 150 200, 180 40, 230 40 C 280 40, 310 200, 360 200" stroke="#E37862" stroke-width="5" fill="none"/>
<text x="290" y="110" font-size="22" fill="#E37862" font-weight="bold">agent</text>
<!-- Decision Boundary -->
<line x1="180" y1="200" x2="180" y2="10" stroke="#999" stroke-width="2" stroke-dasharray="8,5"/>
<text x="180" y="-5" font-size="16" fill="#777" text-anchor="middle">decision boundary</text>
<circle cx="210" cy="200" r="6" fill="#ECA233"/>
<text x="210" y="180" font-family="Georgia" font-style="italic" font-size="20" fill="#ECA233" text-anchor="middle">g_obs</text>
<text x="180" y="280" font-size="18" fill="#555" text-anchor="middle">Positive gap shifts score toward agent traffic.</text>
</g>
</g>
<!-- ========================================================= -->
<!-- COLUMN 3: THE SOLUTION (CONTAMINATION & DR-RL) -->
<!-- ========================================================= -->
<text x="1340" y="80" font-family="Georgia, serif" font-size="28" font-weight="bold" fill="#333333">3. Robust Control &amp; Contamination</text>
<line x1="1340" y1="100" x2="1860" y2="100" stroke="#DDDDDD" stroke-width="2"/>
<!-- Top: Contamination Generator -->
<g transform="translate(1340, 130)">
<text x="0" y="30" font-size="24" font-weight="bold" fill="#444">Contamination Generator G(α)</text>
<!-- Boxes -->
<rect x="20" y="70" width="200" height="50" fill="#D0E5E0" filter="url(#shadow)" rx="6"/>
<text x="120" y="100" font-size="18" fill="#222" text-anchor="middle">labeled human sessions</text>
<rect x="280" y="70" width="200" height="50" fill="#EAD0C8" filter="url(#shadow)" rx="6"/>
<text x="380" y="100" font-size="18" fill="#222" text-anchor="middle">synthetic agent sessions</text>
<!-- Arrows -->
<line x1="120" y1="130" x2="200" y2="180" stroke="#888" stroke-width="3" marker-end="url(#arrow-dark)"/>
<line x1="380" y1="130" x2="300" y2="180" stroke="#888" stroke-width="3" marker-end="url(#arrow-dark)"/>
<!-- Mixed Batch -->
<rect x="150" y="190" width="200" height="50" fill="#F4E9CD" filter="url(#shadow)" rx="6"/>
<text x="250" y="220" font-size="18" fill="#222" text-anchor="middle">mixed batch for training</text>
<!-- Alpha Bar -->
<text x="250" y="275" font-family="Georgia, serif" font-size="20" fill="#555" text-anchor="middle">alpha = 0.33</text>
<rect x="50" y="290" width="268" height="30" fill="#4EA5D9"/>
<rect x="318" y="290" width="132" height="30" fill="#E37862"/>
<text x="184" y="340" font-size="18" fill="#4EA5D9" text-anchor="middle">human share (1-α)</text>
<text x="384" y="340" font-size="18" fill="#E37862" text-anchor="middle">agent share (α)</text>
</g>
<!-- Bottom: Distributionally Robust Control -->
<g transform="translate(1340, 600)">
<text x="0" y="30" font-size="24" font-weight="bold" fill="#444">Distributionally Robust Control Layer</text>
<text x="0" y="80" font-family="Georgia, serif" font-style="italic" font-size="22" fill="#8C7A6B">
π* = arg max<tspan font-size="16" dy="5">π</tspan> min<tspan font-size="16" dy="0">Q ∈ U<tspan font-size="12" dy="5">ε</tspan></tspan>
<tspan dy="-10"> E</tspan><tspan font-size="16" dy="5">d ~ Q</tspan>
<tspan dy="-5">[ R(p,d) - λ COI</tspan><tspan font-size="16" dy="5">leak</tspan><tspan dy="-5">(p,τ') ]</tspan>
</text>
<!-- Ambiguity Ball -->
<g transform="translate(140, 260)">
<line x1="-130" y1="0" x2="130" y2="0" stroke="#CCC" stroke-width="2"/>
<line x1="0" y1="-130" x2="0" y2="130" stroke="#CCC" stroke-width="2"/>
<circle cx="0" cy="0" r="110" stroke="#C4A45B" stroke-width="4" fill="rgba(196,164,91,0.06)"/>
<text x="-95" y="-120" font-family="Georgia" font-style="italic" font-size="24" fill="#C4A45B">U<tspan font-size="16" dy="5">ε</tspan></text>
<!-- Points -->
<circle cx="0" cy="0" r="7" fill="#4EA5D9"/>
<text x="12" y="24" font-family="Georgia" font-style="italic" font-size="22" fill="#4EA5D9">P&#770;<tspan font-size="14" dy="5">N</tspan></text>
<circle cx="-60" cy="-40" r="7" fill="#E37862"/>
<text x="-140" y="-50" font-family="Georgia" font-style="italic" font-size="18" fill="#E37862">worst-case Q*</text>
<circle cx="50" cy="-70" r="6" fill="#85B589"/>
<circle cx="70" cy="50" r="6" fill="#85B589"/>
<circle cx="-40" cy="80" r="6" fill="#85B589"/>
</g>
<!-- Process Steps -->
<g transform="translate(320, 140)">
<rect x="0" y="0" width="220" height="45" fill="#FDEFEF" filter="url(#light-shadow)" rx="6"/>
<text x="110" y="28" font-size="16" fill="#E37862" font-weight="bold" text-anchor="middle">inner min picks Q*</text>
<line x1="110" y1="55" x2="110" y2="85" stroke="#999" stroke-width="2" marker-end="url(#arrow-dark)"/>
<rect x="0" y="95" width="220" height="45" fill="#F4E9CD" filter="url(#light-shadow)" rx="6"/>
<text x="110" y="123" font-size="16" fill="#9E8033" font-weight="bold" text-anchor="middle">sample demand from Q*</text>
<line x1="110" y1="150" x2="110" y2="180" stroke="#999" stroke-width="2" marker-end="url(#arrow-dark)"/>
<rect x="0" y="190" width="220" height="45" fill="#E6F2ED" filter="url(#light-shadow)" rx="6"/>
<text x="110" y="218" font-size="16" fill="#428062" font-weight="bold" text-anchor="middle">outer max updates policy</text>
</g>
<text x="250" y="440" font-size="18" fill="#555" text-anchor="middle">Reward is evaluated on demand drawn from Q*, then used for the policy step.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/static/videos/DefenseOpening.mp4 vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/static/videos/SystemLoopScene.mp4 vendored Normal file

Binary file not shown.

BIN
docs/static/videos/TakeawayScene.mp4 vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
__all__ = ["evaluate", "make_env", "train_qtable", "train_sb3"]

152
engine/backends/common.py Normal file
View File

@@ -0,0 +1,152 @@
from __future__ import annotations
from typing import Any, Mapping
import numpy as np
def make_env(cfg: Mapping[str, Any]):
from gymnasium.wrappers import FlattenObservation
from ..lib.wrappers import EconomicMetricsWrapper
from ..wrapper import PHANTOM
env = PHANTOM(
n_products=int(cfg["n_products"]),
alpha=float(cfg["alpha"]),
N=int(cfg["N"]),
price_bounds=(float(cfg["price_low"]), float(cfg["price_high"])),
lambda_coi=float(cfg["lambda_coi"]),
robust_radius=float(cfg["robust_radius"]),
robust_points=int(cfg["robust_points"]),
robust_rollouts=int(cfg.get("robust_rollouts", 1)),
info_value=float(cfg["info_value"]),
eta_ux=float(cfg.get("eta_ux", 0.5)),
reward_profit_weight=float(cfg.get("reward_profit_weight", 1.0)),
action_levels=int(cfg["action_levels"]),
action_scale_low=float(cfg["action_scale_low"]),
action_scale_high=float(cfg["action_scale_high"]),
max_steps=int(cfg.get("max_steps", 100)),
margin_floor=float(cfg.get("margin_floor", 0.05)),
margin_floor_patience=int(cfg.get("margin_floor_patience", 5)),
render_mode=None,
)
env = EconomicMetricsWrapper(env)
return FlattenObservation(env)
def _action(agent: Any, obs: Any, deterministic: bool = True):
out = agent.predict(obs, deterministic=deterministic)
action = out[0] if isinstance(out, tuple) else out
if isinstance(action, np.ndarray) and action.size == 1:
return int(action.reshape(-1)[0])
return action
def _evaluate_env(agent: Any, env: Any, episodes: int) -> dict[str, float]:
rewards: list[float] = []
revenues: list[float] = []
margins: list[float] = []
coi_levels: list[float] = []
coi_leakages: list[float] = []
volatilities: list[float] = []
agent_probs: list[float] = []
for _ in range(int(episodes)):
obs, _ = env.reset()
done = False
ep_reward = 0.0
ep_revenue = 0.0
ep_margin = 0.0
ep_coi = 0.0
ep_coi_leakage = 0.0
ep_volatility = 0.0
ep_agent_prob = 0.0
steps = 0
while not done:
obs, reward, term, trunc, info = env.step(_action(agent, obs, True))
done = bool(term or trunc)
econ = info.get("economics", {})
ep_reward += float(reward)
ep_revenue += float(econ.get("revenue", info.get("revenue", 0.0)))
ep_margin += float(econ.get("margin", 0.0))
ep_coi += float(econ.get("coi_level", 0.0))
ep_coi_leakage += float(econ.get("coi_leakage", 0.0))
ep_volatility += float(econ.get("volatility", 0.0))
ep_agent_prob += float(econ.get("agent_prob", info.get("agent_prob", 0.0)))
steps += 1
rewards.append(ep_reward)
revenues.append(ep_revenue)
denom = max(steps, 1)
margins.append(ep_margin / denom)
coi_levels.append(ep_coi / denom)
coi_leakages.append(ep_coi_leakage / denom)
volatilities.append(ep_volatility / denom)
agent_probs.append(ep_agent_prob / denom)
return {
"eval/reward_mean": float(np.mean(rewards)) if rewards else 0.0,
"eval/reward_std": float(np.std(rewards)) if rewards else 0.0,
"eval/revenue_mean": float(np.mean(revenues)) if revenues else 0.0,
"eval/revenue_std": float(np.std(revenues)) if revenues else 0.0,
"eval/margin_mean": float(np.mean(margins)) if margins else 0.0,
"eval/coi_level_mean": float(np.mean(coi_levels)) if coi_levels else 0.0,
"eval/coi_leakage_mean": float(np.mean(coi_leakages)) if coi_leakages else 0.0,
"eval/volatility_mean": float(np.mean(volatilities)) if volatilities else 0.0,
"eval/agent_prob_mean": float(np.mean(agent_probs)) if agent_probs else 0.0,
}
def evaluate(
agent: Any,
env: Any,
episodes: int,
cfg: Mapping[str, Any] | None = None,
) -> dict[str, float]:
metrics = _evaluate_env(agent, env, episodes)
if cfg is None or not bool(cfg.get("robust_eval_enabled", True)):
return metrics
nominal_alpha = float(cfg.get("alpha", 0.0))
eval_radius = max(float(cfg.get("robust_radius", 0.0)), 0.15)
low_alpha = float(np.clip(nominal_alpha - eval_radius, 0.0, 1.0))
high_alpha = float(np.clip(nominal_alpha + eval_radius, 0.0, 1.0))
shifted_episodes = max(1, int(np.ceil(int(episodes) / 2)))
shifted_rows = []
for tag, alpha in (
("low", low_alpha),
("nominal", nominal_alpha),
("high", high_alpha),
):
eval_cfg = dict(cfg)
eval_cfg["alpha"] = float(alpha)
shifted_env = make_env(eval_cfg)
shifted_metrics = _evaluate_env(agent, shifted_env, shifted_episodes)
shifted_env.close()
shifted_rows.append((tag, alpha, shifted_metrics))
metrics["eval/robust_alpha_low"] = low_alpha
metrics["eval/robust_alpha_high"] = high_alpha
metrics["eval/robust_reward_worst"] = float(
min(row[2]["eval/reward_mean"] for row in shifted_rows)
)
metrics["eval/robust_revenue_worst"] = float(
min(row[2]["eval/revenue_mean"] for row in shifted_rows)
)
metrics["eval/robust_coi_leakage_worst"] = float(
max(row[2]["eval/coi_leakage_mean"] for row in shifted_rows)
)
for tag, alpha, shifted_metrics in shifted_rows:
metrics[f"eval/{tag}_alpha"] = float(alpha)
metrics[f"eval/{tag}_reward_mean"] = float(shifted_metrics["eval/reward_mean"])
metrics[f"eval/{tag}_revenue_mean"] = float(
shifted_metrics["eval/revenue_mean"]
)
metrics[f"eval/{tag}_coi_leakage_mean"] = float(
shifted_metrics["eval/coi_leakage_mean"]
)
return metrics

131
engine/backends/qtable.py Normal file
View File

@@ -0,0 +1,131 @@
from __future__ import annotations
import logging
import time
from typing import Any, Mapping
import numpy as np
from .common import evaluate, make_env
from ..telemetry.wandb import get_wandb_module
logger = logging.getLogger(__name__)
def train_qtable(
cfg: Mapping[str, Any],
) -> tuple[object, dict[str, Any]]:
from ..lib.discrete import EventQTable
np.random.seed(int(cfg["seed"]))
env = make_env(cfg)
eval_env = make_env(cfg)
agent = EventQTable(
env.action_space.n,
int(cfg["n_products"]),
(float(cfg["price_low"]), float(cfg["price_high"])),
lr=float(cfg["q_lr"]),
gamma=float(cfg["gamma"]),
n_bins=int(cfg["q_bins"]),
)
total_reward = 0.0
total_revenue = 0.0
steps = 0
epsilon = float(cfg["eps_start"])
log_freq = max(1, int(cfg.get("log_freq", 100)))
console_progress = bool(cfg.get("console_progress", False))
obs, _ = env.reset(seed=int(cfg["seed"]))
started_at = time.perf_counter()
wandb = get_wandb_module()
wandb_live = bool(wandb is not None and wandb.run is not None)
step_offset = max(0, int(cfg.get("wandb_step_offset", 0)))
interval_sums = {
"reward": 0.0,
"revenue": 0.0,
"agent_prob": 0.0,
"alpha_adv": 0.0,
"coi_leakage": 0.0,
}
interval_count = 0
train_events: list[dict[str, float | int]] = []
for _ in range(int(cfg["total_timesteps"])):
action, state = agent.act(obs, epsilon)
nxt, reward, term, trunc, info = env.step(action)
done = bool(term or trunc)
agent.update(state, action, float(reward), agent.encode(nxt), done)
total_reward += float(reward)
revenue = float(info.get("economics", {}).get("revenue", 0.0))
total_revenue += revenue
steps += 1
interval_sums["reward"] += float(reward)
interval_sums["revenue"] += revenue
interval_sums["agent_prob"] += float(info.get("agent_prob", 0.0))
interval_sums["alpha_adv"] += float(info.get("alpha_adv", 0.0))
interval_sums["coi_leakage"] += float(info.get("coi_leakage", 0.0))
interval_count += 1
if steps % log_freq == 0 and interval_count > 0:
denom = float(interval_count)
event = {
"train/reward_mean": interval_sums["reward"] / denom,
"train/revenue_mean": interval_sums["revenue"] / denom,
"train/agent_prob": interval_sums["agent_prob"] / denom,
"train/alpha_adv": interval_sums["alpha_adv"] / denom,
"train/coi_leakage": interval_sums["coi_leakage"] / denom,
"train/epsilon": float(epsilon),
"train/global_step": int(steps),
}
if wandb_live:
wandb.log(dict(event), step=step_offset + int(steps))
else:
train_events.append(event)
if console_progress:
elapsed = max(time.perf_counter() - started_at, 1e-6)
speed = steps / elapsed
logger.info(
"step=%d/%d reward=%.3f revenue=%.3f eps=%.4f speed=%.1f steps/s",
steps,
int(cfg["total_timesteps"]),
event["train/reward_mean"],
event["train/revenue_mean"],
event["train/epsilon"],
speed,
)
interval_sums = {key: 0.0 for key in interval_sums}
interval_count = 0
epsilon = max(float(cfg["eps_end"]), epsilon * float(cfg["eps_decay"]))
obs = env.reset()[0] if done else nxt
if interval_count > 0:
denom = float(interval_count)
tail_event = {
"train/reward_mean": interval_sums["reward"] / denom,
"train/revenue_mean": interval_sums["revenue"] / denom,
"train/agent_prob": interval_sums["agent_prob"] / denom,
"train/alpha_adv": interval_sums["alpha_adv"] / denom,
"train/coi_leakage": interval_sums["coi_leakage"] / denom,
"train/epsilon": float(epsilon),
"train/global_step": int(steps),
}
if wandb_live:
wandb.log(dict(tail_event), step=step_offset + int(steps))
else:
train_events.append(tail_event)
metrics: dict[str, Any] = {
"train/reward_mean": total_reward / max(steps, 1),
"train/revenue_mean": total_revenue / max(steps, 1),
"train/epsilon": float(epsilon),
"train/global_step": int(cfg["total_timesteps"]),
}
metrics.update(evaluate(agent, eval_env, int(cfg["eval_episodes"]), cfg=cfg))
metrics["_train_events"] = train_events
env.close()
eval_env.close()
return agent, metrics

188
engine/backends/sb3.py Normal file
View File

@@ -0,0 +1,188 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Mapping
from ..lib.callbacks import MetricsCallback
from .common import evaluate, make_env
def _net_arch(name: Any) -> list[int]:
presets = {
"tiny": [32, 32],
"small": [64, 64],
"medium": [128, 128],
"large": [256, 256],
}
if isinstance(name, (list, tuple)):
return [int(v) for v in name]
raw = str(name).lower().strip()
if raw in presets:
return presets[raw]
if "x" in raw:
try:
parsed = [int(v) for v in raw.split("x") if v]
return parsed if parsed else presets["small"]
except ValueError:
return presets["small"]
return presets["small"]
def _activation(name: Any):
try:
import torch.nn as nn
except ImportError:
return None
return {
"relu": nn.ReLU,
"tanh": nn.Tanh,
"elu": nn.ELU,
"leaky_relu": nn.LeakyReLU,
}.get(str(name).lower().strip(), nn.ReLU)
def _policy_kwargs(cfg: Mapping[str, Any]) -> dict[str, Any]:
kwargs: dict[str, Any] = {"net_arch": _net_arch(cfg.get("arch", "small"))}
activation = _activation(cfg.get("activation", "relu"))
if activation is not None:
kwargs["activation_fn"] = activation
return kwargs
def build_model(cfg: Mapping[str, Any], env: Any):
try:
from stable_baselines3 import A2C, DQN, PPO
except ImportError as exc:
raise ImportError("stable-baselines3 is required for SB3 algorithms") from exc
algo = str(cfg["algo"])
policy_kwargs = _policy_kwargs(cfg)
device = str(cfg.get("device", "auto"))
seed = int(cfg["seed"])
if algo == "sac":
raise ValueError("sac is not supported with the discrete core env")
if algo == "ppo":
return PPO(
"MlpPolicy",
env,
verbose=1,
device=device,
policy_kwargs=policy_kwargs,
seed=seed,
learning_rate=float(cfg["learning_rate"]),
n_steps=int(cfg["n_steps"]),
batch_size=int(cfg["batch_size"]),
n_epochs=int(cfg["n_epochs"]),
gamma=float(cfg["gamma"]),
gae_lambda=float(cfg["gae_lambda"]),
clip_range=float(cfg["clip_range"]),
ent_coef=float(cfg["ent_coef"]),
)
if algo == "a2c":
return A2C(
"MlpPolicy",
env,
verbose=1,
device=device,
policy_kwargs=policy_kwargs,
seed=seed,
learning_rate=float(cfg["learning_rate"]),
n_steps=max(5, int(cfg["n_steps"]) // 32),
gamma=float(cfg["gamma"]),
gae_lambda=float(cfg["gae_lambda"]),
ent_coef=float(cfg["ent_coef"]),
)
if algo == "dqn":
return DQN(
"MlpPolicy",
env,
verbose=1,
device=device,
policy_kwargs=policy_kwargs,
seed=seed,
learning_rate=float(cfg["learning_rate"]),
buffer_size=int(cfg["buffer_size"]),
batch_size=int(cfg["batch_size"]),
gamma=float(cfg["gamma"]),
train_freq=int(cfg["train_freq"]),
learning_starts=int(cfg["learning_starts"]),
target_update_interval=int(cfg["target_update_interval"]),
exploration_fraction=float(cfg["exploration_fraction"]),
exploration_final_eps=float(cfg["exploration_final_eps"]),
)
raise ValueError(f"unsupported algo '{algo}'")
def train_sb3(cfg: Mapping[str, Any]) -> tuple[object, dict[str, Any]]:
try:
from stable_baselines3.common.callbacks import EvalCallback
from stable_baselines3.common.monitor import Monitor
except ImportError as exc:
raise ImportError("stable-baselines3 is required for SB3 models") from exc
env = Monitor(make_env(cfg))
eval_env = Monitor(make_env(cfg))
model = build_model(cfg, env)
try:
import torch
print(
"PHANTOM_DEVICE: "
+ json.dumps(
{
"requested": str(cfg.get("device", "auto")),
"torch_cuda_available": bool(torch.cuda.is_available()),
"torch_device_count": int(torch.cuda.device_count()),
"sb3_device": str(getattr(model, "device", "unknown")),
}
)
)
except Exception:
pass
metrics_callback = MetricsCallback(
log_histograms=False,
log_freq=int(cfg["log_freq"]),
step_offset=int(cfg.get("wandb_step_offset", 0)),
)
callbacks = [metrics_callback]
callbacks.append(
EvalCallback(
eval_env,
eval_freq=int(cfg["eval_freq"]),
n_eval_episodes=int(cfg["eval_episodes"]),
deterministic=True,
verbose=0,
)
)
target_steps = int(cfg["total_timesteps"])
remaining_steps = max(0, target_steps - int(getattr(model, "num_timesteps", 0)))
if remaining_steps > 0:
model.learn(
total_timesteps=remaining_steps,
callback=callbacks,
reset_num_timesteps=False,
)
model_dir = Path(str(cfg["model_dir"]))
model_dir.mkdir(parents=True, exist_ok=True)
model_path = model_dir / f"phantom_{cfg['algo']}"
model.save(str(model_path))
metrics: dict[str, Any] = evaluate(
model,
eval_env,
int(cfg["eval_episodes"]),
cfg=cfg,
)
metrics["train/global_step"] = int(model.num_timesteps)
metrics["model/path"] = str(model_path.with_suffix(".zip"))
metrics["_train_events"] = list(metrics_callback.events)
env.close()
eval_env.close()
return model, metrics

625
engine/benchmark.py Normal file
View File

@@ -0,0 +1,625 @@
from __future__ import annotations
import argparse
import json
import logging
import os
from datetime import datetime, UTC
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from .lib.tiers import LinearElasticityPolicy, StaticPolicy, SurgePolicy
from .logging_utils import configure_logging
from .spec import TrainSpec
from .telemetry.wandb import get_wandb_module
wandb = get_wandb_module()
HAS_WANDB = wandb is not None
logger = logging.getLogger(__name__)
def _log(message: str) -> None:
logger.info(message)
def _parse_list(raw: str) -> list[str]:
return [x.strip().lower() for x in str(raw).split(",") if x.strip()]
def _parse_float_list(raw: str) -> list[float]:
return [float(x.strip()) for x in str(raw).split(",") if x.strip()]
def _truthy(value: str | bool | None) -> bool:
if isinstance(value, bool):
return value
if value is None:
return False
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def _action(policy, obs: np.ndarray):
out = policy.predict(obs, deterministic=True)
action = out[0] if isinstance(out, tuple) else out
if isinstance(action, np.ndarray) and action.size == 1:
return int(action.reshape(-1)[0])
return int(action)
def _run_eval_episode(env, policy) -> dict:
obs, _ = env.reset()
done = False
total_reward = 0.0
total_revenue = 0.0
total_margin = 0.0
total_coi = 0.0
price_trace: list[float] = []
step_count = 0
while not done:
action = _action(policy, obs)
obs, reward, term, trunc, info = env.step(action)
done = bool(term or trunc)
econ = info.get("economics", {})
total_reward += float(reward)
total_revenue += float(econ.get("revenue", 0.0))
total_margin += float(econ.get("margin", 0.0))
total_coi += float(econ.get("coi_level", 0.0))
prices = np.asarray(info.get("prices", []), dtype=np.float32)
if prices.size > 0:
price_trace.append(float(np.mean(prices)))
step_count += 1
denom = max(step_count, 1)
return {
"reward": total_reward,
"revenue": total_revenue,
"mean_margin": total_margin / denom,
"mean_coi": total_coi / denom,
"price_trace": price_trace,
}
def _build_tier(name: str, cfg: dict, alpha: float, *, step_offset: int = 0):
from .backends.common import make_env
tier = name.lower().strip()
run_cfg = dict(cfg)
run_cfg["alpha"] = float(alpha)
run_cfg["wandb_step_offset"] = int(step_offset)
if tier == "static":
return StaticPolicy(int(run_cfg["action_levels"])), []
if tier == "surge":
return (
SurgePolicy(
n_actions=int(run_cfg["action_levels"]),
n_products=int(run_cfg["n_products"]),
),
[],
)
if tier == "linear":
warmup_env = make_env(run_cfg)
policy = LinearElasticityPolicy(
n_actions=int(run_cfg["action_levels"]),
n_products=int(run_cfg["n_products"]),
price_low=float(run_cfg["price_low"]),
price_high=float(run_cfg["price_high"]),
)
policy.fit(
warmup_env,
warmup_steps=int(run_cfg.get("linear_warmup_steps", 800)),
seed=int(run_cfg["seed"]),
)
warmup_env.close()
return policy, []
if tier == "qtable":
from .backends.qtable import train_qtable
run_cfg["console_progress"] = True
agent, metrics = train_qtable(run_cfg)
events = metrics.get("_train_events", [])
return agent, events if isinstance(events, list) else []
if tier in {"ppo", "a2c", "dqn"}:
from .backends.sb3 import train_sb3
run_cfg["algo"] = tier
agent, metrics = train_sb3(run_cfg)
events = metrics.get("_train_events", [])
return agent, events if isinstance(events, list) else []
raise ValueError(f"unsupported tier '{name}'")
def _log_train_events(
events: list[dict],
*,
tier_name: str,
mode_label: str,
alpha: float,
step_offset: int,
) -> int:
if not (HAS_WANDB and wandb.run is not None):
return int(step_offset)
if not events:
return int(step_offset)
ordered = sorted(
[evt for evt in events if isinstance(evt, dict)],
key=lambda evt: int(evt.get("train/global_step", 0)),
)
if not ordered:
return int(step_offset)
cursor = int(step_offset)
for evt in ordered:
rel_step = max(1, int(evt.get("train/global_step", 0)))
payload = dict(evt)
payload.update(
{
"run.kind": "benchmark",
"runtime/backend": tier_name,
"study/mode": mode_label,
"study/no_robust": float(mode_label == "no_robust"),
"study/alpha": float(alpha),
}
)
wandb.log(payload, step=cursor + rel_step)
max_rel = max(max(1, int(evt.get("train/global_step", 0))) for evt in ordered)
return cursor + max_rel + 1
def run_benchmark(
cfg: dict,
tiers: list[str],
alpha_values: list[float],
n_episodes: int,
mode_label: str,
step_cursor_start: int = 0,
):
from .backends.common import make_env
rows: list[dict] = []
traces: list[dict] = []
total_runs = max(1, len(alpha_values) * len(tiers))
run_index = 0
wandb_step_cursor = int(step_cursor_start)
for alpha in alpha_values:
for tier_name in tiers:
run_index += 1
_log(
f"[{run_index}/{total_runs}] alpha={float(alpha):.2f} tier={tier_name}: training"
)
policy, train_events = _build_tier(
tier_name,
cfg,
alpha,
step_offset=wandb_step_cursor,
)
prev_cursor = int(wandb_step_cursor)
wandb_step_cursor = _log_train_events(
train_events,
tier_name=tier_name,
mode_label=mode_label,
alpha=float(alpha),
step_offset=wandb_step_cursor,
)
if wandb_step_cursor == prev_cursor and tier_name in {
"qtable",
"ppo",
"a2c",
"dqn",
}:
wandb_step_cursor += max(1, int(cfg.get("total_timesteps", 1))) + 1
env = make_env({**cfg, "alpha": float(alpha)})
eps = [_run_eval_episode(env, policy) for _ in range(int(n_episodes))]
env.close()
row = {
"tier": tier_name,
"mode": mode_label,
"alpha": float(alpha),
"episodes": int(n_episodes),
"mean_reward": float(np.mean([e["reward"] for e in eps])),
"mean_revenue": float(np.mean([e["revenue"] for e in eps])),
"mean_margin": float(np.mean([e["mean_margin"] for e in eps])),
"mean_coi": float(np.mean([e["mean_coi"] for e in eps])),
"std_revenue": float(np.std([e["revenue"] for e in eps])),
}
row["objective_score"] = row["mean_reward"]
rows.append(row)
_log(
f"[{run_index}/{total_runs}] alpha={float(alpha):.2f} tier={tier_name}: "
f"reward={row['mean_reward']:.3f} revenue={row['mean_revenue']:.3f} "
f"coi={row['mean_coi']:.4f} score={row['objective_score']:.3f}"
)
max_len = max((len(e["price_trace"]) for e in eps), default=0)
step_means = []
for step in range(max_len):
vals = [
e["price_trace"][step] for e in eps if step < len(e["price_trace"])
]
step_means.append(float(np.mean(vals)) if vals else np.nan)
traces.append(
{
"tier": tier_name,
"alpha": float(alpha),
"mean_price_trace": step_means,
}
)
if HAS_WANDB and wandb.run is not None:
wandb.log(
{
"run.kind": "benchmark",
"runtime/backend": tier_name,
"study/mode": mode_label,
"study/no_robust": float(mode_label == "no_robust"),
"study/alpha": float(alpha),
"eval/reward_mean": row["mean_reward"],
"eval/revenue_mean": row["mean_revenue"],
"eval/margin_mean": row["mean_margin"],
"eval/coi_level_mean": row["mean_coi"],
"objective/score": row["objective_score"],
"objective/coi_preserved": row["mean_coi"],
},
step=wandb_step_cursor,
)
wandb_step_cursor += 1
return pd.DataFrame(rows), traces, int(wandb_step_cursor)
def _plot_outputs(df: pd.DataFrame, traces: list[dict], out_dir: Path, stamp: str):
fig1 = plt.figure(figsize=(11, 4.5))
if "mode" in df.columns:
groups = sorted(df[["tier", "mode"]].drop_duplicates().values.tolist())
for tier, mode in groups:
sub = df[(df["tier"] == tier) & (df["mode"] == mode)].sort_values("alpha")
plt.plot(
sub["alpha"],
sub["mean_revenue"],
marker="o",
label=f"{tier}:{mode}",
)
else:
for tier in sorted(df["tier"].unique()):
sub = df[df["tier"] == tier].sort_values("alpha")
plt.plot(sub["alpha"], sub["mean_revenue"], marker="o", label=tier)
plt.xlabel("contamination alpha")
plt.ylabel("mean episode revenue")
plt.title("Revenue under contamination")
plt.grid(alpha=0.3)
plt.legend()
fig1.tight_layout()
rev_path = out_dir / f"benchmark_revenue_{stamp}.png"
fig1.savefig(rev_path, dpi=220)
plt.close(fig1)
fig2 = plt.figure(figsize=(11, 4.5))
if "mode" in df.columns:
groups = sorted(df[["tier", "mode"]].drop_duplicates().values.tolist())
for tier, mode in groups:
sub = df[(df["tier"] == tier) & (df["mode"] == mode)].sort_values("alpha")
plt.plot(
sub["alpha"],
sub["mean_coi"],
marker="s",
label=f"{tier}:{mode}",
)
else:
for tier in sorted(df["tier"].unique()):
sub = df[df["tier"] == tier].sort_values("alpha")
plt.plot(sub["alpha"], sub["mean_coi"], marker="s", label=tier)
plt.xlabel("contamination alpha")
plt.ylabel("mean COI level")
plt.title("COI preservation")
plt.grid(alpha=0.3)
plt.legend()
fig2.tight_layout()
coi_path = out_dir / f"benchmark_coi_{stamp}.png"
fig2.savefig(coi_path, dpi=220)
plt.close(fig2)
focus_alpha = float(df["alpha"].min()) if not df.empty else 0.0
alpha_traces = [t for t in traces if abs(float(t["alpha"]) - focus_alpha) < 1e-9]
fig3 = plt.figure(figsize=(11, 4.5))
for item in alpha_traces:
xs = np.arange(len(item["mean_price_trace"]))
ys = np.asarray(item["mean_price_trace"], dtype=np.float32)
mode = item.get("mode")
label = f"{item['tier']}:{mode}" if mode is not None else str(item["tier"])
plt.plot(xs, ys, label=label)
plt.xlabel("step")
plt.ylabel("mean price")
plt.title(f"Price evolution (alpha={focus_alpha:.2f})")
plt.grid(alpha=0.3)
plt.legend()
fig3.tight_layout()
price_path = out_dir / f"benchmark_price_trace_{stamp}.png"
fig3.savefig(price_path, dpi=220)
plt.close(fig3)
return rev_path, coi_path, price_path
def _run_with_args(args, compare_robust_override: bool | None = None):
compare_robust = (
bool(compare_robust_override)
if compare_robust_override is not None
else _truthy(os.environ.get("PHANTOM_BENCHMARK_COMPARE_ROBUST"))
)
robust_modes = [False, True] if compare_robust else [bool(args.no_robust)]
base_overrides = {
"seed": args.seed,
"total_timesteps": args.total_timesteps,
"n_products": args.n_products,
"N": args.N,
"lambda_coi": args.lambda_coi,
"robust_radius": args.robust_radius,
"robust_points": args.robust_points,
"robust_rollouts": args.robust_rollouts,
"eta_ux": args.eta_ux,
"reward_profit_weight": args.reward_profit_weight,
"price_low": args.price_low,
"price_high": args.price_high,
"action_levels": args.action_levels,
"action_scale_low": args.action_scale_low,
"action_scale_high": args.action_scale_high,
"max_steps": args.max_steps,
"learning_rate": args.learning_rate,
"batch_size": args.batch_size,
"n_steps": args.n_steps,
"linear_warmup_steps": args.linear_warmup_steps,
"device": args.device,
}
tiers = _parse_list(args.tiers)
alpha_values = _parse_float_list(args.alpha_values)
_log(
"starting run "
+ json.dumps(
{
"tiers": tiers,
"alpha_values": alpha_values,
"episodes": int(args.episodes),
"total_timesteps": int(args.total_timesteps),
"device": str(args.device),
}
)
)
all_frames: list[pd.DataFrame] = []
all_traces: list[dict] = []
wandb_step_cursor = 0
for no_robust in robust_modes:
overrides = dict(base_overrides)
overrides["no_robust"] = bool(no_robust)
cfg = TrainSpec.from_flat(
{k: v for k, v in overrides.items() if v is not None}
).to_flat_dict()
cfg["linear_warmup_steps"] = int(args.linear_warmup_steps)
mode_label = "no_robust" if no_robust else "robust"
_log(f"mode={mode_label}: begin")
df_mode, traces_mode, wandb_step_cursor = run_benchmark(
cfg,
tiers,
alpha_values,
args.episodes,
mode_label=mode_label,
step_cursor_start=wandb_step_cursor,
)
_log(f"mode={mode_label}: complete ({len(df_mode)} rows)")
for trace in traces_mode:
trace["mode"] = mode_label
all_frames.append(df_mode)
all_traces.extend(traces_mode)
df = pd.concat(all_frames, ignore_index=True) if all_frames else pd.DataFrame()
traces = all_traces
out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
stamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
csv_path = out_dir / f"benchmark_{stamp}.csv"
trace_path = out_dir / f"benchmark_traces_{stamp}.json"
df.to_csv(csv_path, index=False)
trace_path.write_text(json.dumps(traces, indent=2))
rev_path, coi_path, price_path = _plot_outputs(df, traces, out_dir, stamp)
_log(f"artifacts written in {out_dir}")
if not df.empty:
best_idx = int(df["objective_score"].idxmax())
best = df.iloc[best_idx]
_log(
"BEST_TIER="
+ json.dumps(
{
"tier": best["tier"],
"mode": best.get("mode", "robust"),
"alpha": float(best["alpha"]),
"objective_score": float(best["objective_score"]),
"mean_revenue": float(best["mean_revenue"]),
"mean_coi": float(best["mean_coi"]),
}
)
)
_log(f"BENCHMARK_CSV={csv_path}")
_log(f"BENCHMARK_TRACES={trace_path}")
_log(f"BENCHMARK_PLOT_REVENUE={rev_path}")
_log(f"BENCHMARK_PLOT_COI={coi_path}")
_log(f"BENCHMARK_PLOT_PRICE={price_path}")
def run_cli(raw_args: list[str] | None = None):
configure_logging()
parser = argparse.ArgumentParser(description="PHANTOM benchmark orchestrator")
parser.add_argument("--project", default="capstone")
parser.add_argument("--tiers", default="static,surge,linear,qtable,ppo")
parser.add_argument("--alpha-values", default="0.0,0.3,0.6")
parser.add_argument("--episodes", type=int, default=10)
parser.add_argument("--output-dir", default="engine/studies/results")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--total-timesteps", type=int, default=25_000)
parser.add_argument("--n-products", type=int, default=10)
parser.add_argument("--N", type=int, default=100)
parser.add_argument("--lambda-coi", type=float, default=0.2)
parser.add_argument("--robust-radius", type=float, default=0.15)
parser.add_argument("--robust-points", type=int, default=5)
parser.add_argument("--robust-rollouts", type=int, default=1)
parser.add_argument("--eta-ux", type=float, default=0.5)
parser.add_argument("--reward-profit-weight", type=float, default=1.0)
parser.add_argument("--price-low", type=float, default=10.0)
parser.add_argument("--price-high", type=float, default=150.0)
parser.add_argument("--action-levels", type=int, default=9)
parser.add_argument("--action-scale-low", type=float, default=0.8)
parser.add_argument("--action-scale-high", type=float, default=1.2)
parser.add_argument("--max-steps", type=int, default=100)
parser.add_argument("--learning-rate", type=float, default=3e-4)
parser.add_argument("--batch-size", type=int, default=256)
parser.add_argument("--n-steps", type=int, default=2048)
parser.add_argument("--linear-warmup-steps", type=int, default=800)
parser.add_argument("--device", type=str, default="auto")
parser.add_argument("--no-robust", action="store_true")
parser.add_argument("--no-wandb", action="store_true")
parser.add_argument("--offline", action="store_true")
parser.add_argument("--sweep-agent", action="store_true")
parser.add_argument("--sweep-id", type=str)
parser.add_argument("--count", type=int, default=0)
args = parser.parse_args(raw_args)
if args.sweep_agent:
if args.no_wandb or not HAS_WANDB:
raise ValueError("sweep agent requires wandb")
if not args.sweep_id:
raise ValueError("--sweep-id is required with --sweep-agent")
def _sweep_run():
run = wandb.init(mode="offline" if args.offline else "online")
try:
key_to_attr = {
"tiers": "tiers",
"alpha_values": "alpha_values",
"episodes": "episodes",
"total_timesteps": "total_timesteps",
"lambda_coi": "lambda_coi",
"robust_radius": "robust_radius",
"robust_points": "robust_points",
"robust_rollouts": "robust_rollouts",
"eta_ux": "eta_ux",
"reward_profit_weight": "reward_profit_weight",
"learning_rate": "learning_rate",
"batch_size": "batch_size",
"n_steps": "n_steps",
"no_robust": "no_robust",
"device": "device",
}
for key in (
"tiers",
"alpha_values",
"episodes",
"total_timesteps",
"lambda_coi",
"robust_radius",
"robust_points",
"robust_rollouts",
"eta_ux",
"reward_profit_weight",
"learning_rate",
"batch_size",
"n_steps",
"no_robust",
"device",
):
if key in wandb.config:
setattr(args, key_to_attr[key], wandb.config[key])
_run_with_args(args)
finally:
if run is not None:
wandb.finish()
wandb.agent(
args.sweep_id,
function=_sweep_run,
count=args.count if args.count > 0 else None,
)
return
if args.no_wandb or not HAS_WANDB:
_run_with_args(args)
return
tiers = _parse_list(args.tiers)
alpha_values = _parse_float_list(args.alpha_values)
run_stamp = datetime.now(UTC).strftime("%m%d-%H%M%S")
compare_enabled = _truthy(os.environ.get("PHANTOM_BENCHMARK_COMPARE_ROBUST"))
compare_tag = "robust-compare" if compare_enabled else "single-mode"
modes = (
[("no_robust", True), ("robust", False)]
if compare_enabled
else [("no_robust" if bool(args.no_robust) else "robust", bool(args.no_robust))]
)
run_idx = 0
for tier in tiers:
for mode_label, no_robust in modes:
for alpha in alpha_values:
run_idx += 1
alpha_token = (
f"{float(alpha):.2f}".rstrip("0").rstrip(".").replace(".", "p")
)
tier_args = argparse.Namespace(**vars(args))
tier_args.tiers = tier
tier_args.alpha_values = str(float(alpha))
tier_args.no_robust = bool(no_robust)
run = wandb.init(
project=args.project,
name=(
f"benchmark-{tier}-{mode_label}-a{alpha_token}-{run_stamp}-{run_idx}"
),
tags=[
"benchmark",
compare_tag,
f"backend:{tier}",
f"mode:{mode_label}",
f"alpha:{alpha_token}",
],
config={
"run.kind": "benchmark",
"runtime/backend": tier,
"study/mode": mode_label,
"study/no_robust": float(no_robust),
"study/alpha": float(alpha),
"tiers": tier,
"alpha_values": str(float(alpha)),
"episodes": args.episodes,
"total_timesteps": args.total_timesteps,
"lambda_coi": args.lambda_coi,
"robust_radius": args.robust_radius,
"robust_points": args.robust_points,
"robust_rollouts": args.robust_rollouts,
"eta_ux": args.eta_ux,
"reward_profit_weight": args.reward_profit_weight,
"learning_rate": args.learning_rate,
"device": args.device,
},
mode="offline" if args.offline else "online",
)
try:
_run_with_args(tier_args, compare_robust_override=False)
finally:
if run is not None:
wandb.finish()
if __name__ == "__main__":
run_cli()

View File

@@ -1,66 +1,105 @@
from sys import platform
import numpy as np
from .lib.demand import generate_demand, estimate_demand
from .lib.behavior import sample_behavior
from .lib.demand import generate_demand_for_actor, estimate_demand
from .lib.behavior import get_adjusted_transitions, sample_behavior_from_transitions
from logging import INFO, getLogger
logger = getLogger(__name__)
logger.setLevel(INFO)
class MarketEngine:
"""implements separate demand distributions for humans and agents per Section 3.1.1"""
class MarketEngine():
def __init__(self,
alpha = 0.5,
N = 100,
demand_distribution = (50, 10),
demand_sampling_function = np.random.normal):
self.Nagents = int(N*alpha)
self.Nhumans = int(N*(1-alpha))
self.demand = (demand_sampling_function, demand_distribution)
def __init__(
self,
alpha: float,
N: int,
human_params: tuple,
agent_params: tuple,
demand_distribution=np.random.normal,
noise_std: float = 1.0,
action_weights: dict | None = None,
):
# no defaults for D_H, D_A - force explicit experiment design
self.alpha = alpha
self.N = int(N)
self.Nagents = int(N * alpha)
self.Nhumans = int(N * (1 - alpha))
self.human_params = human_params
self.agent_params = agent_params
self.noise_std = noise_std
self.demand_dist = demand_distribution
self.action_weights = action_weights
def act(self, prices):
demand = generate_demand(prices, *self.demand)
sample_n = lambda n, human: [sample_behavior(demand, human=human) for _ in range(n)]
human_t, agent_t = sample_n(self.Nhumans, True), sample_n(self.Nagents, False)
trajectories = human_t + agent_t
demand_estimate = estimate_demand(trajectories)
return demand_estimate
# generate separate demands d() per actor type
demand_h = generate_demand_for_actor(
prices,
self.human_params,
self.noise_std,
distribution_method=self.demand_dist,
)
demand_a = generate_demand_for_actor(
prices,
self.agent_params,
self.noise_std,
distribution_method=self.demand_dist,
)
human_transitions = get_adjusted_transitions(demand_h, human=True)
agent_transitions = get_adjusted_transitions(demand_a, human=False)
# sample behavior trajectories from each demand distribution
human_t = [
sample_behavior_from_transitions(human_transitions)
for _ in range(self.Nhumans)
]
agent_t = [
sample_behavior_from_transitions(agent_transitions)
for _ in range(self.Nagents)
]
# store trajectories for agent probability calculation
self.last_trajectories = human_t + agent_t
return estimate_demand(self.last_trajectories, self.action_weights)
def measure(self):
pass
class PricingEngine():
def __init__(self,
) -> None:
class PricingEngine:
def __init__(
self,
) -> None:
pass
def act(self, demand):
return np.random.uniform(low=25, high=100, size=10)
class Limbo():
def __init__(self,
platform,
market
) -> None:
class Limbo:
def __init__(self, platform, market) -> None:
self.platform_turn = True
self.platform = platform
self.market = market
self.output = None
def step(self):
# we could code golf this a little bit
if self.platform_turn:
self.output = self.platform.act(self.output)
else:
self.output = self.market.act(self.output)
print(self.output)
self.platform_turn = not self.platform_turn
return self.output
def reset(self):
self.platform_turn = True
self.output = None
if __name__ == "__main__":
platform = PricingEngine()
market = MarketEngine()
market = MarketEngine(
alpha=0.3, N=100, human_params=(50, 10), agent_params=(45, 15)
)
limbo = Limbo(platform, market)
for _ in range(10):
limbo.step()

View File

@@ -1,3 +1,38 @@
from .demand import generate_demand, estimate_demand
from .behavior import sample_behavior
from .render import DashboardRenderer, style_axis
from __future__ import annotations
from importlib import import_module
_EXPORTS: dict[str, tuple[str, str]] = {
"estimate_demand": (".demand", "estimate_demand"),
"estimate_weighted_demand": (".demand", "estimate_weighted_demand"),
"generate_demand_for_actor": (".demand", "generate_demand_for_actor"),
"sample_behavior": (".behavior", "sample_behavior"),
"get_transition_models": (".behavior", "get_transition_models"),
"trajectory_to_events": (".behavior", "trajectory_to_events"),
"DashboardRenderer": (".render", "DashboardRenderer"),
"style_axis": (".render", "style_axis"),
"EconomicMetricsWrapper": (".wrappers", "EconomicMetricsWrapper"),
"MetricsCallback": (".callbacks", "MetricsCallback"),
"EvalMetricsCallback": (".callbacks", "EvalMetricsCallback"),
"ProviderBenchmark": (".providers", "ProviderBenchmark"),
"ProviderResult": (".providers", "ProviderResult"),
"BenchmarkConfig": (".providers", "BenchmarkConfig"),
"RandomBaseline": (".providers", "RandomBaseline"),
"SurgeBaseline": (".providers", "SurgeBaseline"),
"compute_uplift_coi": (".coi", "compute_uplift_coi"),
"extract_purchases": (".coi", "extract_purchases"),
"compute_agent_probability": (".coi", "compute_agent_probability"),
"EventQTable": (".discrete", "EventQTable"),
}
__all__ = sorted(_EXPORTS)
def __getattr__(name: str):
if name not in _EXPORTS:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
module_name, attr_name = _EXPORTS[name]
module = import_module(module_name, package=__name__)
value = getattr(module, attr_name)
globals()[name] = value
return value

View File

@@ -1,27 +1,107 @@
from sim.rl.behavior_loader.models import BehaviorModel, AgentBehaviorModel, aggregate_event_transitions
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[2]))
try:
from sim.rl.behavior_loader.models import (
BehaviorModel,
AgentBehaviorModel,
aggregate_event_transitions,
)
except ImportError:
BehaviorModel = None
AgentBehaviorModel = None
aggregate_event_transitions = None
import pandas as pd
import numpy as np
from .demand import generate_demand
from .demand import generate_demand_for_actor
base_dir = "/home/velocitatem/Documents/Projects/PHANTOM/experiments"
human_dir, agent_dir = f"{base_dir}/collected_data/", f"{base_dir}/agents/collected_data/"
base_dir = Path(__file__).parents[2] / "experiments"
human_dir = str(base_dir / "collected_data")
agent_dir = str(base_dir / "agents" / "collected_data")
_cache = {} # lazy cache for models and base pivots
def _get_base_pivot(human: bool):
key = 'human' if human else 'agent'
if (
BehaviorModel is None
or AgentBehaviorModel is None
or aggregate_event_transitions is None
):
raise ImportError("behavior loader dependencies are unavailable")
key = "human" if human else "agent"
if key not in _cache:
model = BehaviorModel(human_dir) if human else AgentBehaviorModel(agent_dir)
mdp = model.build_MDP()
_cache[key] = pd.DataFrame(aggregate_event_transitions(mdp)).fillna(0.0)
return _cache[key]
def get_transition_models():
"""load human and agent transition models for agent probability calculation
returns:
tuple: (human_transitions, agent_transitions) as dicts of event->event->prob
"""
if (
BehaviorModel is None
or AgentBehaviorModel is None
or aggregate_event_transitions is None
):
raise ImportError("behavior loader dependencies are unavailable")
human_model = BehaviorModel(human_dir)
agent_model = AgentBehaviorModel(agent_dir)
human_mdp = human_model.build_MDP()
agent_mdp = agent_model.build_MDP()
human_trans = aggregate_event_transitions(human_mdp)
agent_trans = aggregate_event_transitions(agent_mdp)
return human_trans, agent_trans
def trajectory_to_events(trajectory: list) -> list:
"""extract event names from trajectory for KL divergence calculation
trajectories are in format 'eventName_product0', extract just eventName
args:
trajectory: list like ['view_product0', 'add_to_cart_product1', 'checkout_product1']
returns:
list: event names like ['view', 'add_to_cart', 'checkout']
"""
events = []
for state in trajectory:
# state format from sample_behavior: 'eventName_productX'
if "_product" in state:
event = state.rsplit("_product", 1)[0]
else:
event = state
events.append(event)
return events
def adjust_behavior_to_condition(condition, transition_matrix):
# expand NxN transition matrix to (N*P)x(N*P) weighted by demand condition
cond_norm = condition / np.sum(condition)
condition = np.asarray(condition, dtype=float)
condition = np.nan_to_num(condition, nan=0.0, posinf=0.0, neginf=0.0)
condition = np.clip(condition, 0.0, None)
s = float(np.sum(condition))
if not np.isfinite(s) or s <= 0:
cond_norm = np.full(len(condition), 1.0 / max(len(condition), 1), dtype=float)
else:
cond_norm = condition / s
n_products = len(condition)
base_vals = transition_matrix.values
base_cols, base_rows = transition_matrix.columns.tolist(), transition_matrix.index.tolist()
base_cols, base_rows = (
transition_matrix.columns.tolist(),
transition_matrix.index.tolist(),
)
# expand via kronecker-like tiling: each cell becomes a P*P block weighted by outer product of cond_norm
expanded = np.kron(base_vals, np.outer(cond_norm, cond_norm))
@@ -29,19 +109,33 @@ def adjust_behavior_to_condition(condition, transition_matrix):
new_rows = [f"{r}_product{p}" for r in base_rows for p in range(n_products)]
return pd.DataFrame(expanded, index=new_rows, columns=new_cols)
def sample_behavior(condition, human=True, max_len=40):
base_pivot = _get_base_pivot(human)
adjusted_transitions = adjust_behavior_to_condition(condition, base_pivot)
def get_adjusted_transitions(condition, human=True):
base_pivot = _get_base_pivot(human)
return adjust_behavior_to_condition(condition, base_pivot)
def sample_behavior_from_transitions(adjusted_transitions, max_len=40):
trajectory = [np.random.choice(adjusted_transitions.index)]
while len(trajectory) < max_len or 'checkout' in trajectory[-1]:
probs = adjusted_transitions.loc[trajectory[-1]].values
sample = np.random.choice(adjusted_transitions.columns, p=probs/np.sum(probs) if np.sum(probs) > 0 else None)
while len(trajectory) < max_len and "checkout" not in trajectory[-1]:
probs = np.asarray(adjusted_transitions.loc[trajectory[-1]].values, dtype=float)
probs = np.nan_to_num(probs, nan=0.0, posinf=0.0, neginf=0.0)
probs = np.clip(probs, 0.0, None)
s = float(np.sum(probs))
sample = np.random.choice(
adjusted_transitions.columns, p=(probs / s) if s > 0 else None
)
trajectory.append(sample)
return trajectory
def sample_behavior(condition, human=True, max_len=40):
adjusted_transitions = get_adjusted_transitions(condition, human=human)
return sample_behavior_from_transitions(adjusted_transitions, max_len=max_len)
if __name__ == "__main__":
t=sample_behavior(generate_demand(np.array([10,20,30])), human=True)
t = sample_behavior(generate_demand_for_actor(np.array([10, 20, 30])), human=True)
print(t)
t=sample_behavior(generate_demand(np.array([10,20,30])), human=False)
t = sample_behavior(generate_demand_for_actor(np.array([10, 20, 30])), human=False)
print(t)

148
engine/lib/callbacks.py Normal file
View File

@@ -0,0 +1,148 @@
"""Training callbacks with algorithm-agnostic metric extraction."""
from typing import Any
from stable_baselines3.common.callbacks import BaseCallback, EvalCallback
import numpy as np
from ..telemetry.wandb import get_wandb_module
class MetricsCallback(BaseCallback):
"""Collects interval train metrics from env info dictionaries."""
def __init__(
self,
log_histograms: bool = False,
log_freq: int = 100,
step_offset: int = 0,
verbose: int = 0,
):
super().__init__(verbose)
self.log_histograms = log_histograms
self.log_freq = max(1, int(log_freq))
self.step_offset = max(0, int(step_offset))
self._wandb = get_wandb_module()
self._wandb_live = bool(self._wandb is not None and self._wandb.run is not None)
self._window_sums = {
"train/revenue_mean": 0.0,
"train/margin_mean": 0.0,
"train/coi_level_mean": 0.0,
"train/regret_mean": 0.0,
"train/profit_mean": 0.0,
"train/agent_prob": 0.0,
"train/alpha_adv": 0.0,
"train/ux_penalty": 0.0,
"train/volatility": 0.0,
"train/coi_mix": 0.0,
"train/coi_base": 0.0,
"train/coi_leakage": 0.0,
"train/coi_penalty": 0.0,
}
self._window_count = 0
self.events: list[dict[str, Any]] = []
def _accumulate(self, info: dict[str, Any]) -> None:
econ = info.get("economics")
if not isinstance(econ, dict):
return
self._window_sums["train/revenue_mean"] += float(econ.get("revenue", 0.0))
self._window_sums["train/margin_mean"] += float(econ.get("margin", 0.0))
self._window_sums["train/coi_level_mean"] += float(econ.get("coi_level", 0.0))
self._window_sums["train/regret_mean"] += float(econ.get("regret", 0.0))
if "profit" in econ:
self._window_sums["train/profit_mean"] += float(econ.get("profit", 0.0))
if "agent_prob" in econ:
self._window_sums["train/agent_prob"] += float(econ.get("agent_prob", 0.0))
if "alpha_adv" in econ:
self._window_sums["train/alpha_adv"] += float(econ.get("alpha_adv", 0.0))
if "ux_penalty" in econ:
self._window_sums["train/ux_penalty"] += float(econ.get("ux_penalty", 0.0))
if "volatility" in econ:
self._window_sums["train/volatility"] += float(econ.get("volatility", 0.0))
if "coi_mix" in econ:
self._window_sums["train/coi_mix"] += float(econ.get("coi_mix", 0.0))
if "coi_base" in econ:
self._window_sums["train/coi_base"] += float(econ.get("coi_base", 0.0))
if "coi_leakage" in econ:
self._window_sums["train/coi_leakage"] += float(
econ.get("coi_leakage", 0.0)
)
if "coi_penalty" in econ:
self._window_sums["train/coi_penalty"] += float(
econ.get("coi_penalty", 0.0)
)
self._window_count += 1
def _flush(self, step: int) -> None:
if self._window_count <= 0:
return
denom = float(self._window_count)
payload = {
key: (value / denom)
for key, value in self._window_sums.items()
if value != 0.0
or key
in {
"train/revenue_mean",
"train/margin_mean",
"train/coi_level_mean",
"train/regret_mean",
}
}
payload["train/global_step"] = int(step)
if self._wandb_live:
self._wandb.log(dict(payload), step=self.step_offset + int(step))
else:
self.events.append(payload)
for key in self._window_sums:
self._window_sums[key] = 0.0
self._window_count = 0
def _on_step(self) -> bool:
for info in self.locals.get("infos", []):
if isinstance(info, dict):
self._accumulate(info)
if self.num_timesteps % self.log_freq == 0:
self._flush(step=self.num_timesteps)
return True
def _on_training_end(self) -> None:
self._flush(step=self.num_timesteps)
class EvalMetricsCallback(EvalCallback):
"""Deterministic evaluation collector detached from logging backends."""
def __init__(
self, eval_env, eval_freq: int = 1000, n_eval_episodes: int = 5, **kwargs
):
super().__init__(
eval_env, eval_freq=eval_freq, n_eval_episodes=n_eval_episodes, **kwargs
)
self._eval_revenues: list[float] = []
self.events: list[dict[str, float | int]] = []
def _on_step(self) -> bool:
result = super()._on_step()
if self.n_calls % self.eval_freq == 0 and hasattr(self, "last_mean_reward"):
self.events.append(
{
"eval/reward_mean": float(self.last_mean_reward),
"eval/revenue_mean": float(np.mean(self._eval_revenues))
if self._eval_revenues
else 0.0,
"train/global_step": int(self.num_timesteps),
}
)
self._eval_revenues = []
return result
def _log_success_callback(self, locals_: dict, globals_: dict) -> None:
# called after each eval episode
info = locals_.get("info", {})
if "economics" in info:
self._eval_revenues.append(info["economics"]["revenue"])

79
engine/lib/coi.py Normal file
View File

@@ -0,0 +1,79 @@
import numpy as np
from typing import Dict
def compute_agent_probability(
trajectory: list,
human_transitions: Dict,
agent_transitions: Dict,
temperature: float = 1.0,
) -> float:
"""estimate agent probability via KL divergence between trajectory transitions and reference models
compares empirical trajectory transition distribution to human/agent prototypes
args:
trajectory: list of state/event strings from session
human_transitions: reference transition dict from human MDP (event->event->prob)
agent_transitions: reference transition dict from agent MDP (event->event->prob)
returns:
agent probability in [0, 1] via softmax over KL divergences
"""
if len(trajectory) < 2:
return 0.0 # insufficient data, assume human
# build empirical transition distribution from trajectory
trans_counts = {}
for s, s_next in zip(trajectory[:-1], trajectory[1:]):
if s not in trans_counts:
trans_counts[s] = {}
trans_counts[s][s_next] = trans_counts[s].get(s_next, 0) + 1
# normalize to probabilities
empirical = {}
for s, nxt in trans_counts.items():
total = sum(nxt.values())
empirical[s] = {s_n: cnt / total for s_n, cnt in nxt.items()}
# compute KL divergence to each prototype
def kl_div(p_dist: Dict, q_dist: Dict) -> float:
eps = 1e-10
# aggregate over all source states in empirical dist
kl = 0.0
for s in p_dist:
if s not in q_dist:
continue # skip states not in reference
p_trans, q_trans = p_dist[s], q_dist[s]
for k in p_trans:
p_val = p_trans[k] + eps
q_val = q_trans.get(k, 0.0) + eps
kl += p_val * np.log(p_val / q_val)
return kl
kl_human = kl_div(empirical, human_transitions)
kl_agent = kl_div(empirical, agent_transitions)
# convert to probability via softmax (lower KL = higher prob)
t = float(max(temperature, 1e-6))
exp_h = np.exp(-kl_human / t)
exp_a = np.exp(-kl_agent / t)
return float(exp_a / (exp_h + exp_a + 1e-10))
def extract_purchases(trajectories: list) -> Dict[int, int]:
purchases: Dict[int, int] = {}
for traj in trajectories:
if traj and "checkout" in traj[-1] and "_product" in traj[-1]:
prod_id = int(traj[-1].rsplit("_product", 1)[1])
purchases[prod_id] = purchases.get(prod_id, 0) + 1
return purchases
def compute_uplift_coi(
prices: np.ndarray, purchases: Dict[int, int], baseline_prices: np.ndarray
) -> float:
# TODO: consider view-weighted fractional purchase for denser signal
return float(
sum(max(0.0, prices[k] - baseline_prices[k]) * n for k, n in purchases.items())
)

View File

@@ -1,45 +1,92 @@
import logging
import numpy as np
from logging import getLogger
logger = getLogger(__name__)
def generate_demand(prices, distribution_method = np.random.normal, distribution_params = (50.0, 10.0)):
# assumption 1: each product has an intrinsic valuation drawn from a normal distribution centered at 50
product_valuations = distribution_method(*distribution_params, size=len(prices))
# assumption 2: demand decreases as price increases, following a simple linear model
demand = np.maximum(0, product_valuations - prices) # demand cannot be negative
CATEGORY_WEIGHTS = {"cart": 4.0, "dwell": 2.0, "nav": 1.0, "filter": 0.5}
ACTION_CATEGORIES = {
"cart": {"add_item", "add_to_cart", "remove", "checkout", "purchase"},
"dwell": {"hover_title", "hover_paragraph", "hover_link"},
"nav": {"page_view", "view_item", "view", "learn_more"},
"filter": {"search", "filter_date", "filter_price", "sort"},
}
DEFAULT_ACTION_WEIGHTS = {
a: CATEGORY_WEIGHTS[c] for c, actions in ACTION_CATEGORIES.items() for a in actions
}
def generate_demand_for_actor(
prices: np.ndarray,
params: tuple,
noise_std: float = 1.0,
distribution_method=np.random.normal,
) -> np.ndarray:
"""d(p;0) = max(0, valuation - price) + epsi for single actor type
params: (mean, std) for valuation distribution D_H or D_A"""
val = distribution_method(*params, size=len(prices))
noise = distribution_method(0, noise_std, len(prices))
demand = np.maximum(0, val - prices + noise)
total = np.sum(demand)
demand = demand / total * 100 if total > 0 else demand # normalize to percentage, avoid div by zero
logger.info(f"Generated demand for prices {prices}: {demand} with valuations from distribution {distribution_params}")
return demand
return demand / total * 100 if total > 0 else demand
def estimate_demand(trajectories):
demand_estimate = {}
def estimate_demand(trajectories, action_weights=None):
return estimate_weighted_demand(trajectories, action_weights)
def _parse_event_state(state: str):
if "_product" not in state:
return state, None
action, raw_pid = state.rsplit("_product", 1)
return action, int(raw_pid) if raw_pid.isdigit() else None
def _weight_for_action(action: str, action_weights: dict) -> float:
if action in action_weights:
return action_weights[action]
if action.startswith("hover"):
return CATEGORY_WEIGHTS["dwell"]
if action.startswith("filter") or action in {"search", "sort"}:
return CATEGORY_WEIGHTS["filter"]
if action.startswith("add") or action in {"checkout", "purchase", "remove"}:
return CATEGORY_WEIGHTS["cart"]
return CATEGORY_WEIGHTS["nav"]
def estimate_weighted_demand(trajectories, action_weights=None):
action_weights = (
DEFAULT_ACTION_WEIGHTS if action_weights is None else action_weights
)
scores = {}
for traj in trajectories:
for event in traj:
if 'view_product' in event:
product_id = int(event.split('_')[-1].replace('product', ''))
demand_estimate[product_id] = demand_estimate.get(product_id, 0) + 1
total_views = sum(demand_estimate.values())
for product_id in demand_estimate:
demand_estimate[product_id] = (demand_estimate[product_id] / total_views) * 100 # normalize to percentage
return demand_estimate
for state in traj:
action, product_id = _parse_event_state(state)
if product_id is None:
continue
w = _weight_for_action(action, action_weights)
if w <= 0:
continue
scores[product_id] = scores.get(product_id, 0.0) + w
total = sum(scores.values())
return (
{pid: (score / total) * 100 for pid, score in scores.items()}
if total > 0
else {}
)
# Example usage
if __name__ == "__main__":
np.random.seed(42)
prices = np.array([20.0, 35.0, 50.0, 65.0])
demand = generate_demand(prices)
print("Generated Demand:", demand)
# demo actor-specific demands
human_params, agent_params = (50, 10), (45, 15)
demand_h = generate_demand_for_actor(prices, human_params)
demand_a = generate_demand_for_actor(prices, agent_params)
print("Human Demand:", demand_h)
print("Agent Demand:", demand_a)
from .behavior import sample_behavior
N, alphat =200, 0.1
trajectories = []
for _ in range(int(N*(1 - alphat))):
trajectories.append(sample_behavior(demand, human=True))
for _ in range(int(N*alphat)):
trajectories.append(sample_behavior(demand, human=False))
demand_estimate = estimate_demand(trajectories)
N, alpha = 200, 0.3
n_h, n_a = int(N * (1 - alpha)), int(N * alpha)
human_t = [sample_behavior(demand_h, human=True) for _ in range(n_h)]
agent_t = [sample_behavior(demand_a, human=False) for _ in range(n_a)]
demand_estimate = estimate_demand(human_t + agent_t)
print("Estimated Demand from Behavior:", demand_estimate)
delta = {k: demand_estimate.get(k, 0) - demand[i] for i, k in enumerate(range(len(prices)))}
delta = np.mean([np.abs(v) for v in delta.values()])
print("Demand Delta:", delta)

70
engine/lib/discrete.py Normal file
View File

@@ -0,0 +1,70 @@
from collections import defaultdict
import gymnasium as gym
from gymnasium import spaces
import numpy as np
class DiscretePriceActionWrapper(gym.ActionWrapper):
def __init__(
self,
env: gym.Env,
n_levels: int = 9,
min_scale: float = 0.8,
max_scale: float = 1.2,
):
super().__init__(env)
self.scales = np.linspace(min_scale, max_scale, n_levels, dtype=np.float32)
self.action_space = spaces.Discrete(n_levels)
def action(self, action: int):
scale = float(self.scales[int(action)])
cur = np.asarray(self.env.unwrapped._prices, dtype=np.float32)
lo, hi = self.env.unwrapped.price_bounds
return np.clip(cur * scale, lo, hi).astype(np.float32)
class EventQTable:
def __init__(
self,
n_actions: int,
n_products: int,
price_bounds: tuple,
lr: float = 0.1,
gamma: float = 0.99,
n_bins: int = 6,
):
self.n_actions = int(n_actions)
self.n_products = int(n_products)
self.lr = float(lr)
self.gamma = float(gamma)
self.q = defaultdict(lambda: np.zeros(self.n_actions, dtype=np.float32))
lo, hi = price_bounds
self.demand_bins = np.linspace(0.0, 100.0, n_bins + 1)[1:-1]
self.price_bins = np.linspace(lo, hi, n_bins + 1)[1:-1]
def encode(self, obs: np.ndarray) -> tuple:
obs = np.asarray(obs, dtype=np.float32)
d = obs[: self.n_products]
p = obs[self.n_products : 2 * self.n_products]
d_mean = float(np.mean(d)) if d.size else 0.0
d_std = float(np.std(d)) if d.size else 0.0
p_mean = float(np.mean(p)) if p.size else 0.0
return (
int(np.digitize(d_mean, self.demand_bins)),
int(np.digitize(d_std, self.demand_bins)),
int(np.digitize(p_mean, self.price_bins)),
)
def act(self, obs: np.ndarray, eps: float = 0.0) -> tuple[int, tuple]:
s = self.encode(obs)
if np.random.random() < eps:
return int(np.random.randint(self.n_actions)), s
return int(np.argmax(self.q[s])), s
def update(self, s: tuple, a: int, r: float, s2: tuple, done: bool):
target = r + (0.0 if done else self.gamma * float(np.max(self.q[s2])))
self.q[s][a] += self.lr * (target - self.q[s][a])
def predict(self, obs: np.ndarray, deterministic: bool = True):
a, _ = self.act(obs, 0.0 if deterministic else 0.05)
return a, None

182
engine/lib/providers.py Normal file
View File

@@ -0,0 +1,182 @@
"""Provider benchmarking - compare pricing strategies across contamination levels."""
from dataclasses import dataclass, field
from typing import Callable, Any
import numpy as np
import pandas as pd
try:
import wandb
HAS_WANDB = True
except ImportError:
HAS_WANDB = False
class RandomBaseline:
"""uniform random action selection as a lower-bound baseline"""
def __init__(self, n_actions: int):
self.n = n_actions
def __call__(self, obs):
return int(np.random.randint(self.n))
def predict(self, obs, **kw):
return self(obs), None
class SurgeBaseline:
"""heuristic surge pricing: boost price when demand is above threshold, discount when below.
matches the naive pricing rule from thesis Section 3.3.2"""
def __init__(
self, n_actions: int, high_threshold: float = 60.0, low_threshold: float = 30.0
):
self.n = n_actions
self.mid = n_actions // 2 # identity action (scale ~1.0)
self.high_t = high_threshold
self.low_t = low_threshold
def __call__(self, obs):
obs = np.asarray(obs, dtype=np.float32)
n_prod = len(obs) // 2
demand_mean = float(np.mean(obs[:n_prod])) if n_prod > 0 else 0.0
if demand_mean >= self.high_t:
return min(self.mid + 2, self.n - 1) # surge: two levels above identity
if demand_mean <= self.low_t:
return max(self.mid - 2, 0) # discount: two levels below identity
return self.mid # hold
def predict(self, obs, **kw):
return self(obs), None
@dataclass
class ProviderResult:
"""Single benchmark result for one provider at one alpha level."""
name: str
alpha: float
total_revenue: float
mean_revenue: float
coi_level: float
coi_preserved_pct: float # vs alpha=0 baseline
margin_integrity: float
regret: float
episodes: int
@dataclass
class BenchmarkConfig:
"""Configuration for provider benchmark runs."""
n_episodes: int = 100
alpha_range: list[float] = field(default_factory=lambda: [0.0, 0.1, 0.3, 0.5])
baseline_name: str = "fixed"
class ProviderBenchmark:
"""Compare pricing providers to prove margin preservation across contamination levels.
Usage:
def env_factory(alpha):
return EconomicMetricsWrapper(PHANTOM(alpha=alpha))
providers = {
"fixed": lambda obs: np.ones(10) * 50,
"learned": model.predict,
}
benchmark = ProviderBenchmark(env_factory, providers)
results = benchmark.run()
print(benchmark.summary_table())
"""
def __init__(
self,
env_factory: Callable[[float], Any],
providers: dict[str, Callable],
config: BenchmarkConfig | None = None,
):
self.env_factory = env_factory # fn(alpha) -> wrapped env
self.providers = providers # {name: fn(obs) -> action}
self.config = config or BenchmarkConfig()
self.results: list[ProviderResult] = []
def run(self) -> list[ProviderResult]:
"""Run benchmark across all providers and alpha levels."""
baseline_coi: dict[str, float] = {} # {provider: coi at alpha=0}
for alpha in self.config.alpha_range:
env = self.env_factory(alpha)
for name, policy_fn in self.providers.items():
revenues, coi_levels, margins = [], [], []
for _ in range(self.config.n_episodes):
obs, _ = env.reset()
episode_revenue = 0.0
done = False
while not done:
action = policy_fn(obs)
# handle sb3 model.predict returning tuple
if isinstance(action, tuple):
action = action[0]
obs, reward, term, trunc, info = env.step(action)
done = term or trunc
econ = info.get("economics", {})
episode_revenue += econ.get("revenue", 0)
coi_levels.append(econ.get("coi_level", 0))
margins.append(econ.get("margin", 0))
revenues.append(episode_revenue)
mean_coi = np.mean(coi_levels) if coi_levels else 0.0
if alpha == 0.0:
baseline_coi[name] = mean_coi
base = baseline_coi.get(name, mean_coi)
coi_preserved = mean_coi / base if base > 0 else 1.0
result = ProviderResult(
name=name,
alpha=alpha,
total_revenue=float(np.sum(revenues)),
mean_revenue=float(np.mean(revenues)),
coi_level=mean_coi,
coi_preserved_pct=coi_preserved * 100,
margin_integrity=float(np.mean(margins)) if margins else 0.0,
regret=0.0, # compute vs optimal if known
episodes=self.config.n_episodes,
)
self.results.append(result)
# log to wandb if available
if HAS_WANDB and wandb.run is not None:
wandb.log(
{
f"benchmark/{name}/revenue": result.mean_revenue,
f"benchmark/{name}/coi_preserved": result.coi_preserved_pct,
f"benchmark/{name}/margin": result.margin_integrity,
"benchmark/alpha": alpha,
}
)
return self.results
def to_dataframe(self) -> pd.DataFrame:
"""Convert results to pandas DataFrame."""
return pd.DataFrame([r.__dict__ for r in self.results])
def summary_table(self) -> pd.DataFrame:
"""Pivot table: providers x alpha with revenue/COI metrics."""
df = self.to_dataframe()
return df.pivot_table(
index="name",
columns="alpha",
values=["mean_revenue", "coi_preserved_pct", "margin_integrity"],
aggfunc="mean",
)

View File

@@ -1,15 +1,19 @@
"""rendering logic for PHANTOM environment dashboard"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
def style_axis(ax, title: str = None, xlabel: str = None, ylabel: str = None):
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
if title: ax.set_title(title, fontsize=11, fontweight='bold', pad=8)
if xlabel: ax.set_xlabel(xlabel, fontsize=9)
if ylabel: ax.set_ylabel(ylabel, fontsize=9)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
if title:
ax.set_title(title, fontsize=11, fontweight="bold", pad=8)
if xlabel:
ax.set_xlabel(xlabel, fontsize=9)
if ylabel:
ax.set_ylabel(ylabel, fontsize=9)
class DashboardRenderer:
@@ -23,13 +27,25 @@ class DashboardRenderer:
if self.fig is None:
plt.ion()
self.fig = plt.figure(figsize=(14, 10))
self.gs = GridSpec(3, 3, figure=self.fig, hspace=0.35, wspace=0.3,
left=0.07, right=0.95, top=0.92, bottom=0.08)
self.gs = GridSpec(
3,
3,
figure=self.fig,
hspace=0.35,
wspace=0.3,
left=0.07,
right=0.95,
top=0.92,
bottom=0.08,
)
plt.show(block=False)
self.fig.clear()
self.fig.suptitle(f'PHANTOM Market Dynamics [t={env._step_count}, a={env.alpha:.2f}]',
fontsize=14, fontweight='bold')
self.fig.suptitle(
f"PHANTOM Market Dynamics [t={env._step_count}, a={env.alpha:.2f}]",
fontsize=14,
fontweight="bold",
)
demand_mat = np.array(env._demand_history).T
price_mat = np.array(env._price_history).T
@@ -51,40 +67,56 @@ class DashboardRenderer:
prices_flat = np.array(env._price_history).flatten()
demands_flat = np.array(env._demand_history).flatten()
product_ids = np.tile(np.arange(env.n_products), len(env._price_history))
ax.scatter(prices_flat, demands_flat, c=product_ids, cmap='plasma', alpha=0.6, s=15, edgecolors='none')
ax.scatter(
prices_flat,
demands_flat,
c=product_ids,
cmap="plasma",
alpha=0.6,
s=15,
edgecolors="none",
)
if len(prices_flat) > 1:
z = np.polyfit(prices_flat, demands_flat, 1)
p_line = np.linspace(prices_flat.min(), prices_flat.max(), 50)
ax.plot(p_line, np.polyval(z, p_line), '--', lw=1.5, alpha=0.8)
ax.plot(p_line, np.polyval(z, p_line), "--", lw=1.5, alpha=0.8)
style_axis(ax, "Price-Demand Relationship", "Price ($)", "Demand")
def _render_elasticity_bar(self, env, elasticity):
ax = self.fig.add_subplot(self.gs[0, 1])
ax.barh(range(env.n_products), elasticity, alpha=0.8)
ax.axvline(0, lw=0.8, alpha=0.5)
ax.axvline(-1, lw=1, ls='--', alpha=0.5)
ax.axvline(-1, lw=1, ls="--", alpha=0.5)
ax.set_yticks(range(env.n_products))
ax.set_yticklabels([f'P{i}' for i in range(env.n_products)], fontsize=7)
ax.set_yticklabels([f"P{i}" for i in range(env.n_products)], fontsize=7)
style_axis(ax, "Price Elasticity", "(dQ/dP)(P/Q)", None)
def _render_session_pie(self, env):
ax = self.fig.add_subplot(self.gs[0, 2])
n_h, n_a = env.market.Nhumans, env.market.Nagents
wedges, _ = ax.pie([n_h, n_a], startangle=90, wedgeprops={'linewidth': 2, 'edgecolor': 'white'})
ax.legend(wedges, [f'H ({n_h})', f'A ({n_a})'], loc='lower center', fontsize=8,
frameon=False, bbox_to_anchor=(0.5, -0.05))
ax.set_title("Session Mix", fontsize=11, fontweight='bold')
wedges, _ = ax.pie(
[n_h, n_a], startangle=90, wedgeprops={"linewidth": 2, "edgecolor": "white"}
)
ax.legend(
wedges,
[f"H ({n_h})", f"A ({n_a})"],
loc="lower center",
fontsize=8,
frameon=False,
bbox_to_anchor=(0.5, -0.05),
)
ax.set_title("Session Mix", fontsize=11, fontweight="bold")
def _render_price_heatmap(self, price_mat):
ax = self.fig.add_subplot(self.gs[1, :2])
im = ax.imshow(price_mat, aspect='auto', cmap='viridis', origin='lower')
im = ax.imshow(price_mat, aspect="auto", cmap="viridis", origin="lower")
style_axis(ax, "Price Heatmap P(product, t)", "Step", "Product")
cbar = self.fig.colorbar(im, ax=ax, fraction=0.03, pad=0.02)
cbar.set_label('$', fontsize=8)
cbar.set_label("$", fontsize=8)
def _render_demand_heatmap(self, demand_mat):
ax = self.fig.add_subplot(self.gs[1, 2])
im = ax.imshow(demand_mat, aspect='auto', cmap='Blues', origin='lower')
im = ax.imshow(demand_mat, aspect="auto", cmap="Blues", origin="lower")
style_axis(ax, "Demand Q(product, t)", "Step", None)
self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02)
@@ -92,11 +124,11 @@ class DashboardRenderer:
ax = self.fig.add_subplot(self.gs[2, 0])
if price_mat.shape[1] > 2:
corr = np.corrcoef(price_mat, demand_mat)[:n_products, n_products:]
im = ax.imshow(corr, cmap='RdBu', vmin=-1, vmax=1, aspect='auto')
im = ax.imshow(corr, cmap="RdBu", vmin=-1, vmax=1, aspect="auto")
ax.set_xticks(range(n_products))
ax.set_yticks(range(n_products))
ax.set_xticklabels([f'Q{i}' for i in range(n_products)], fontsize=6)
ax.set_yticklabels([f'P{i}' for i in range(n_products)], fontsize=6)
ax.set_xticklabels([f"Q{i}" for i in range(n_products)], fontsize=6)
ax.set_yticklabels([f"P{i}" for i in range(n_products)], fontsize=6)
self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02)
style_axis(ax, "Price-Demand Correlation", None, None)
@@ -105,20 +137,27 @@ class DashboardRenderer:
n_steps = len(env._revenue_history)
demand_std = [np.std(d) for d in env._demand_history]
ax.fill_between(range(n_steps), env._revenue_history, alpha=0.3)
ax.plot(env._revenue_history, linewidth=2, label='Revenue')
ax.plot(env._revenue_history, linewidth=2, label="Revenue")
ax.set_xlim(0, max(n_steps, 1))
ax.set_ylim(0, max(env._revenue_history) * 1.1 if env._revenue_history else 1)
ax2 = ax.twinx()
ax2.plot(range(n_steps), demand_std, linewidth=2, ls='-', alpha=0.9, label='sigma(Demand)')
ax2.plot(
range(n_steps),
demand_std,
linewidth=2,
ls="-",
alpha=0.9,
label="sigma(Demand)",
)
d_min, d_max = min(demand_std), max(demand_std)
margin = (d_max - d_min) * 0.2 if d_max > d_min else 0.5
ax2.set_ylim(max(0, d_min - margin), d_max + margin)
ax2.set_ylabel('Demand sigma', fontsize=9)
ax2.set_ylabel("Demand sigma", fontsize=9)
style_axis(ax, "Revenue & Demand Dispersion", "Step", "Revenue ($)")
ax.legend(loc='upper left', fontsize=7, frameon=False)
ax2.legend(loc='upper right', fontsize=7, frameon=False)
ax.legend(loc="upper left", fontsize=7, frameon=False)
ax2.legend(loc="upper right", fontsize=7, frameon=False)
def close(self):
if self.fig:

101
engine/lib/tiers.py Normal file
View File

@@ -0,0 +1,101 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
import numpy as np
class PolicyLike(Protocol):
def predict(self, obs: np.ndarray, deterministic: bool = True): ...
class StaticPolicy:
def __init__(self, n_actions: int):
self._action = int(max(0, n_actions // 2))
def predict(self, obs: np.ndarray, deterministic: bool = True):
return self._action, None
class SurgePolicy:
def __init__(
self,
n_actions: int,
n_products: int,
high_threshold: float = 60.0,
low_threshold: float = 30.0,
):
self.n_actions = int(n_actions)
self.n_products = int(n_products)
self.mid = self.n_actions // 2
self.high_t = float(high_threshold)
self.low_t = float(low_threshold)
def predict(self, obs: np.ndarray, deterministic: bool = True):
obs_arr = np.asarray(obs, dtype=np.float32)
demand = obs_arr[: self.n_products]
demand_mean = float(np.mean(demand)) if demand.size > 0 else 0.0
if demand_mean >= self.high_t:
return min(self.mid + 2, self.n_actions - 1), None
if demand_mean <= self.low_t:
return max(self.mid - 2, 0), None
return self.mid, None
@dataclass
class LinearElasticityPolicy:
n_actions: int
n_products: int
price_low: float
price_high: float
def __post_init__(self):
self.n_actions = int(self.n_actions)
self.n_products = int(self.n_products)
self.price_low = float(self.price_low)
self.price_high = float(self.price_high)
self._target_price = 0.5 * (self.price_low + self.price_high)
self._action_scales = np.linspace(0.8, 1.2, self.n_actions)
def fit(self, env, warmup_steps: int = 800, seed: int = 42):
rng = np.random.default_rng(int(seed))
obs, _ = env.reset(seed=int(seed))
prices: list[float] = []
demands: list[float] = []
for _ in range(int(max(10, warmup_steps))):
action = int(rng.integers(0, self.n_actions))
obs, _, term, trunc, info = env.step(action)
done = bool(term or trunc)
p = np.asarray(info.get("prices", []), dtype=np.float32)
d = np.asarray(info.get("demand", []), dtype=np.float32)
if p.size > 0 and d.size > 0:
prices.append(float(np.mean(p)))
demands.append(float(np.mean(d)))
if done:
obs, _ = env.reset()
if len(prices) < 8:
self._target_price = 0.5 * (self.price_low + self.price_high)
return self
slope, intercept = np.polyfit(np.asarray(prices), np.asarray(demands), 1)
if slope < -1e-6:
p_star = -intercept / (2.0 * slope)
self._target_price = float(np.clip(p_star, self.price_low, self.price_high))
else:
self._target_price = 0.5 * (self.price_low + self.price_high)
return self
def predict(self, obs: np.ndarray, deterministic: bool = True):
obs_arr = np.asarray(obs, dtype=np.float32)
cur_prices = obs_arr[self.n_products : 2 * self.n_products]
cur_mean = (
float(np.mean(cur_prices)) if cur_prices.size > 0 else self._target_price
)
scale = self._target_price / max(cur_mean, 1e-6)
action = int(np.argmin(np.abs(self._action_scales - scale)))
return int(np.clip(action, 0, self.n_actions - 1)), None

90
engine/lib/wrappers.py Normal file
View File

@@ -0,0 +1,90 @@
"""Economic metrics wrapper - calculates thesis-aligned KPIs and injects into info dict."""
import gymnasium as gym
import numpy as np
class EconomicMetricsWrapper(gym.Wrapper):
"""Calculates thesis-aligned economic metrics per step, injects into info.
Metrics follow thesis definitions:
- COI level: E[P] - p_min (Definition 1)
- Margin: (avg_price - p_min) / avg_price
- Regret: 1 - (revenue / baseline_revenue)
"""
def __init__(
self, env: gym.Env, p_min: float = 10.0, baseline_revenue: float | None = None
):
super().__init__(env)
self.p_min = p_min
self.baseline_revenue = baseline_revenue
self._price_history: list[np.ndarray] = []
self._revenue_history: list[float] = []
def reset(self, **kwargs):
obs, info = self.env.reset(**kwargs)
self._price_history = []
self._revenue_history = []
return obs, info
def step(self, action):
obs, reward, terminated, truncated, info = self.env.step(action)
# extract from unwrapped env
prices = self.env.unwrapped._prices
demand_dict = self.env.unwrapped._demand
demand = np.array([demand_dict.get(i, 0.0) for i in range(len(prices))])
# core calculations
revenue = float(np.sum(prices * demand))
avg_price = float(np.mean(prices))
margin = (avg_price - self.p_min) / max(avg_price, 1e-6)
coi_level = avg_price - self.p_min # E[P] - p_min per thesis Def 1
self._price_history.append(prices.copy())
self._revenue_history.append(revenue)
# regret vs baseline (golden path)
regret = 0.0
if self.baseline_revenue and self.baseline_revenue > 0:
regret = 1.0 - (revenue / self.baseline_revenue)
# inject structured metrics into info
info["economics"] = {
"revenue": revenue,
"margin": margin,
"coi_level": coi_level,
"regret": regret,
}
for key in (
"coi_mix",
"coi_base",
"coi_leakage",
"coi_penalty",
"ux_penalty",
"volatility",
"profit",
"cost_floor",
"reward_revenue",
"reward_total",
"agent_prob",
"alpha_adv",
"alpha_nominal",
):
if key in info:
info["economics"][key] = info[key]
info["prices"] = prices.copy()
info["demand"] = demand.copy()
return obs, reward, terminated, truncated, info
@property
def episode_revenue(self) -> float:
return sum(self._revenue_history)
@property
def episode_mean_price(self) -> float:
if not self._price_history:
return 0.0
return float(np.mean([np.mean(p) for p in self._price_history]))

33
engine/logging_utils.py Normal file
View File

@@ -0,0 +1,33 @@
from __future__ import annotations
import logging
import os
import sys
_CONFIGURED = False
def _resolve_level(raw: str | None) -> int:
name = str(raw or os.environ.get("PHANTOM_LOG_LEVEL", "INFO")).upper().strip()
return int(getattr(logging, name, logging.INFO))
def configure_logging(level: str | None = None) -> None:
global _CONFIGURED
if _CONFIGURED:
return
logger = logging.getLogger("engine")
logger.setLevel(_resolve_level(level))
logger.propagate = False
if logger.handlers:
_CONFIGURED = True
return
handler = logging.StreamHandler(stream=sys.stdout)
handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s")
)
logger.addHandler(handler)
_CONFIGURED = True

View File

@@ -0,0 +1,5 @@
from .benchmark import run_benchmark_cli
from .sweep_agent import run_sweep_agent
from .train import run_train_once
__all__ = ["run_benchmark_cli", "run_sweep_agent", "run_train_once"]

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
def run_benchmark_cli(raw_args: list[str] | None = None) -> None:
from ..benchmark import run_cli
run_cli(raw_args)

View File

@@ -0,0 +1,60 @@
from __future__ import annotations
from typing import Any, Mapping, Sequence
from ..spec import TrainSpec, run_name
from ..telemetry.wandb import (
current_config,
finish_run,
get_wandb_module,
init_run,
run_agent,
)
from .train import run_with_active_sweep_run
def run_sweep_agent(
*,
project: str,
sweep_id: str,
count: int,
offline: bool,
no_wandb: bool,
base_overrides: Mapping[str, Any],
kind: str,
scenario: str,
group: str | None,
extra_tags: Sequence[str],
) -> None:
if no_wandb:
raise ValueError("sweep agent requires wandb")
if not sweep_id:
raise ValueError("--sweep-id is required with --sweep-agent")
if get_wandb_module() is None:
raise ImportError("wandb is required for sweep runs")
mode = "offline" if offline else "online"
def _sweep_trial() -> None:
run = init_run(mode=mode, project=project, group=group, sweep_mode=True)
try:
merged = dict(base_overrides)
merged.update(current_config())
spec = TrainSpec.from_flat(merged)
if run is not None:
run.name = run_name(spec, kind=kind, scenario=scenario)
run_with_active_sweep_run(
spec,
kind=kind,
scenario=scenario,
group=group,
extra_tags=extra_tags,
)
finally:
finish_run()
run_agent(
sweep_id,
_sweep_trial,
count=count if count > 0 else None,
)

View File

@@ -0,0 +1,124 @@
from __future__ import annotations
import json
from typing import Any, Sequence
from ..spec import TrainSpec, run_metadata, run_name
from ..telemetry.wandb import (
finish_run,
get_wandb_module,
init_run,
log_metrics,
update_run_config,
update_summary,
)
from ..train_core import run_train
def _tags_for_run(spec: TrainSpec, kind: str, extra_tags: Sequence[str]) -> list[str]:
tags = [
kind,
spec.algorithm.name,
spec.runtime.backend,
"vanilla" if spec.study.no_robust else "robust",
]
tags.extend([tag for tag in extra_tags if tag])
return tags
def _print_local_metrics(metrics: dict[str, Any]) -> None:
print(json.dumps(metrics, indent=2))
print("PHANTOM_METRICS:" + json.dumps(metrics))
def _log_train_events(events: list[dict[str, Any]], log_freq: int) -> None:
if not events:
return
period = max(1, int(log_freq))
last_logged_step = -period
for event in sorted(
[evt for evt in events if isinstance(evt, dict)],
key=lambda evt: int(evt.get("train/global_step", 0)),
):
step = int(event.get("train/global_step", 0))
if step <= 0 or (step - last_logged_step) < period:
continue
log_metrics(event, step=step)
last_logged_step = step
def run_train_once(
spec: TrainSpec,
*,
project: str,
offline: bool,
no_wandb: bool,
kind: str,
scenario: str,
group: str | None,
extra_tags: Sequence[str],
) -> dict[str, Any]:
wandb = get_wandb_module()
if no_wandb or wandb is None:
result = run_train(spec)
_print_local_metrics(result.metrics)
return result.metrics
mode = "offline" if offline else "online"
tags = _tags_for_run(spec, kind, extra_tags)
metadata = run_metadata(
spec,
kind=kind,
scenario=scenario,
group=group,
tags=tags,
)
config = spec.to_flat_dict()
config.update(metadata)
name = run_name(spec, kind=kind, scenario=scenario)
init_run(
mode=mode,
project=project,
config=config,
name=name,
tags=tags,
group=group,
sweep_mode=False,
)
try:
result = run_train(spec)
_log_train_events(result.events, spec.runtime.log_freq)
metrics = result.metrics
step = int(metrics.get("train/global_step", spec.runtime.total_timesteps))
log_metrics(metrics, step=step)
update_summary(metrics)
return metrics
finally:
finish_run()
def run_with_active_sweep_run(
spec: TrainSpec,
*,
kind: str,
scenario: str,
group: str | None,
extra_tags: Sequence[str],
) -> dict[str, Any]:
tags = _tags_for_run(spec, kind, extra_tags)
metadata = run_metadata(
spec,
kind=kind,
scenario=scenario,
group=group,
tags=tags,
)
update_run_config({**spec.to_flat_dict(), **metadata})
result = run_train(spec)
_log_train_events(result.events, spec.runtime.log_freq)
metrics = result.metrics
step = int(metrics.get("train/global_step", spec.runtime.total_timesteps))
log_metrics(metrics, step=step)
update_summary(metrics)
return metrics

100
engine/project.json Normal file
View File

@@ -0,0 +1,100 @@
{
"$schema": "../node_modules/nx/schemas/project-schema.json",
"name": "research",
"projectType": "application",
"sourceRoot": "engine",
"targets": {
"install": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_research.sh install",
"cwd": "."
}
},
"test": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": ".venv/bin/pytest -v",
"cwd": "."
}
},
"train": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "bash scripts/nx_research.sh train",
"cwd": "."
}
},
"benchmark": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "bash scripts/nx_research.sh benchmark",
"cwd": "."
}
},
"benchmark-simple": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "bash scripts/nx_research.sh benchmark-simple",
"cwd": "."
}
},
"benchmark-agent": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "bash scripts/nx_research.sh benchmark-agent",
"cwd": "."
}
},
"train-agent": {
"executor": "nx:run-commands",
"dependsOn": [
"install"
],
"options": {
"command": "bash scripts/nx_research.sh train-agent",
"cwd": "."
}
},
"train-bootstrap": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_research.sh train-bootstrap",
"cwd": "."
}
},
"stats": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_research.sh stats",
"cwd": "."
}
},
"docker-train-publish": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_research.sh docker-train-publish",
"cwd": "."
}
}
},
"tags": [
"scope:research",
"type:python"
]
}

332
engine/spec.py Normal file
View File

@@ -0,0 +1,332 @@
from __future__ import annotations
from dataclasses import dataclass, field
import os
from typing import Any, Mapping, Sequence
def _truthy(value: str | bool | None) -> bool:
if isinstance(value, bool):
return value
if value is None:
return False
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def _normalize_keys(raw: Mapping[str, Any]) -> dict[str, Any]:
alias_map = {
"algorithm": "algo",
"algorithm.name": "algo",
"env.n_products": "n_products",
"env.action_levels": "action_levels",
"env.action_scale_low": "action_scale_low",
"env.action_scale_high": "action_scale_high",
"env.price_low": "price_low",
"env.price_high": "price_high",
"env.max_steps": "max_steps",
"env.margin_floor": "margin_floor",
"env.margin_floor_patience": "margin_floor_patience",
"env.n_sessions": "N",
"study.alpha": "alpha",
"study.lambda_coi": "lambda_coi",
"study.robust_radius": "robust_radius",
"study.robust_points": "robust_points",
"study.robust_rollouts": "robust_rollouts",
"study.info_value": "info_value",
"study.eta_ux": "eta_ux",
"study.reward_profit_weight": "reward_profit_weight",
"study.revenue_weight": "revenue_weight",
"optimizer.learning_rate": "learning_rate",
"optimizer.gamma": "gamma",
"optimizer.batch_size": "batch_size",
"optimizer.n_steps": "n_steps",
"runtime.backend": "backend",
"runtime.device": "device",
"runtime.seed": "seed",
"runtime.total_timesteps": "total_timesteps",
"runtime.checkpoint_interval": "checkpoint_interval",
"eval.eval_freq": "eval_freq",
"eval.eval_episodes": "eval_episodes",
}
normalized: dict[str, Any] = {}
for key, value in raw.items():
canonical = alias_map.get(str(key), str(key))
normalized[canonical] = value
return normalized
@dataclass(frozen=True)
class AlgorithmSpec:
name: str = "ppo"
@dataclass(frozen=True)
class EnvSpec:
n_products: int = 10
n_sessions: int = 100
price_low: float = 10.0
price_high: float = 150.0
action_levels: int = 9
action_scale_low: float = 0.8
action_scale_high: float = 1.2
max_steps: int = 100
margin_floor: float = 0.05
margin_floor_patience: int = 5
@dataclass(frozen=True)
class StudySpec:
alpha: float = 0.3
lambda_coi: float = 0.2
robust_radius: float = 0.15
robust_points: int = 5
robust_rollouts: int = 1
info_value: float = 1.0
eta_ux: float = 0.5
reward_profit_weight: float = 1.0
revenue_weight: float = 0.01
no_robust: bool = False
@dataclass(frozen=True)
class OptimizerSpec:
learning_rate: float = 3e-4
gamma: float = 0.99
buffer_size: int = 50_000
batch_size: int = 256
tau: float = 0.005
train_freq: int = 1
learning_starts: int = 1_000
target_update_interval: int = 1_000
exploration_fraction: float = 0.2
exploration_final_eps: float = 0.05
n_steps: int = 2_048
n_epochs: int = 10
gae_lambda: float = 0.95
clip_range: float = 0.2
ent_coef: float = 0.0
q_lr: float = 0.1
q_bins: int = 6
eps_start: float = 1.0
eps_end: float = 0.05
eps_decay: float = 0.9995
arch: str = "small"
activation: str = "relu"
vf_coef: float = 0.5
max_grad_norm: float = 0.5
@dataclass(frozen=True)
class RuntimeSpec:
project: str = "capstone"
backend: str = "sb3"
device: str = "auto"
seed: int = 42
total_timesteps: int = 50_000
checkpoint_interval: int = 200_000
model_dir: str = "engine/models"
log_freq: int = 100
@dataclass(frozen=True)
class EvalSpec:
eval_freq: int = 1_000
eval_episodes: int = 5
robust_eval_enabled: bool = True
@dataclass(frozen=True)
class TrainSpec:
algorithm: AlgorithmSpec = field(default_factory=AlgorithmSpec)
env: EnvSpec = field(default_factory=EnvSpec)
study: StudySpec = field(default_factory=StudySpec)
optimizer: OptimizerSpec = field(default_factory=OptimizerSpec)
runtime: RuntimeSpec = field(default_factory=RuntimeSpec)
eval: EvalSpec = field(default_factory=EvalSpec)
def to_flat_dict(self) -> dict[str, Any]:
return {
"project": self.runtime.project,
"algo": self.algorithm.name,
"seed": self.runtime.seed,
"total_timesteps": self.runtime.total_timesteps,
"eval_episodes": self.eval.eval_episodes,
"eval_freq": self.eval.eval_freq,
"log_freq": self.runtime.log_freq,
"model_dir": self.runtime.model_dir,
"backend": self.runtime.backend,
"device": self.runtime.device,
"checkpoint_interval": self.runtime.checkpoint_interval,
"n_products": self.env.n_products,
"N": self.env.n_sessions,
"price_low": self.env.price_low,
"price_high": self.env.price_high,
"action_levels": self.env.action_levels,
"action_scale_low": self.env.action_scale_low,
"action_scale_high": self.env.action_scale_high,
"max_steps": self.env.max_steps,
"margin_floor": self.env.margin_floor,
"margin_floor_patience": self.env.margin_floor_patience,
"alpha": self.study.alpha,
"lambda_coi": self.study.lambda_coi,
"robust_radius": self.study.robust_radius,
"robust_points": self.study.robust_points,
"robust_rollouts": self.study.robust_rollouts,
"info_value": self.study.info_value,
"eta_ux": self.study.eta_ux,
"reward_profit_weight": self.study.reward_profit_weight,
"revenue_weight": self.study.revenue_weight,
"no_robust": self.study.no_robust,
"learning_rate": self.optimizer.learning_rate,
"gamma": self.optimizer.gamma,
"buffer_size": self.optimizer.buffer_size,
"batch_size": self.optimizer.batch_size,
"tau": self.optimizer.tau,
"train_freq": self.optimizer.train_freq,
"learning_starts": self.optimizer.learning_starts,
"target_update_interval": self.optimizer.target_update_interval,
"exploration_fraction": self.optimizer.exploration_fraction,
"exploration_final_eps": self.optimizer.exploration_final_eps,
"n_steps": self.optimizer.n_steps,
"n_epochs": self.optimizer.n_epochs,
"gae_lambda": self.optimizer.gae_lambda,
"clip_range": self.optimizer.clip_range,
"ent_coef": self.optimizer.ent_coef,
"q_lr": self.optimizer.q_lr,
"q_bins": self.optimizer.q_bins,
"eps_start": self.optimizer.eps_start,
"eps_end": self.optimizer.eps_end,
"eps_decay": self.optimizer.eps_decay,
"arch": self.optimizer.arch,
"activation": self.optimizer.activation,
"vf_coef": self.optimizer.vf_coef,
"max_grad_norm": self.optimizer.max_grad_norm,
"robust_eval_enabled": self.eval.robust_eval_enabled,
}
@classmethod
def from_flat(
cls,
raw: Mapping[str, Any] | None = None,
*,
env_vars: Mapping[str, str] | None = None,
) -> "TrainSpec":
base = cls().to_flat_dict()
incoming = _normalize_keys(raw or {})
base.update({k: v for k, v in incoming.items() if v is not None})
runtime_env = os.environ if env_vars is None else env_vars
base["device"] = str(
base.get("device", runtime_env.get("PHANTOM_DEVICE", "auto"))
)
backend = str(base.get("backend", "sb3")).lower()
if backend == "auto":
backend = "sb3"
if backend != "sb3":
backend = "sb3"
no_robust = _truthy(base.get("no_robust"))
if no_robust:
base["lambda_coi"] = 0.0
base["robust_radius"] = 0.0
base["robust_points"] = 1
base["robust_rollouts"] = 1
return cls(
algorithm=AlgorithmSpec(name=str(base["algo"]).lower().strip()),
env=EnvSpec(
n_products=int(base["n_products"]),
n_sessions=int(base["N"]),
price_low=float(base["price_low"]),
price_high=float(base["price_high"]),
action_levels=int(base["action_levels"]),
action_scale_low=float(base["action_scale_low"]),
action_scale_high=float(base["action_scale_high"]),
max_steps=int(base["max_steps"]),
margin_floor=float(base["margin_floor"]),
margin_floor_patience=int(base["margin_floor_patience"]),
),
study=StudySpec(
alpha=float(base["alpha"]),
lambda_coi=float(base["lambda_coi"]),
robust_radius=float(base["robust_radius"]),
robust_points=int(base["robust_points"]),
robust_rollouts=int(base["robust_rollouts"]),
info_value=float(base["info_value"]),
eta_ux=float(base["eta_ux"]),
reward_profit_weight=float(base["reward_profit_weight"]),
revenue_weight=float(base["revenue_weight"]),
no_robust=no_robust,
),
optimizer=OptimizerSpec(
learning_rate=float(base["learning_rate"]),
gamma=float(base["gamma"]),
buffer_size=int(base["buffer_size"]),
batch_size=int(base["batch_size"]),
tau=float(base["tau"]),
train_freq=int(base["train_freq"]),
learning_starts=int(base["learning_starts"]),
target_update_interval=int(base["target_update_interval"]),
exploration_fraction=float(base["exploration_fraction"]),
exploration_final_eps=float(base["exploration_final_eps"]),
n_steps=int(base["n_steps"]),
n_epochs=int(base["n_epochs"]),
gae_lambda=float(base["gae_lambda"]),
clip_range=float(base["clip_range"]),
ent_coef=float(base["ent_coef"]),
q_lr=float(base["q_lr"]),
q_bins=int(base["q_bins"]),
eps_start=float(base["eps_start"]),
eps_end=float(base["eps_end"]),
eps_decay=float(base["eps_decay"]),
arch=str(base["arch"]),
activation=str(base["activation"]),
vf_coef=float(base["vf_coef"]),
max_grad_norm=float(base["max_grad_norm"]),
),
runtime=RuntimeSpec(
project=str(base["project"]),
backend=backend,
device=str(base["device"]),
seed=int(base["seed"]),
total_timesteps=int(base["total_timesteps"]),
checkpoint_interval=int(base["checkpoint_interval"]),
model_dir=str(base["model_dir"]),
log_freq=int(base["log_freq"]),
),
eval=EvalSpec(
eval_freq=int(base["eval_freq"]),
eval_episodes=int(base["eval_episodes"]),
robust_eval_enabled=_truthy(base.get("robust_eval_enabled", True)),
),
)
def run_name(spec: TrainSpec, *, kind: str, scenario: str) -> str:
return (
f"{kind}/{spec.algorithm.name}/{spec.runtime.backend}/"
f"{spec.runtime.device}/{scenario}/s{spec.runtime.seed}"
)
def run_metadata(
spec: TrainSpec,
*,
kind: str,
scenario: str,
group: str | None = None,
tags: Sequence[str] = (),
) -> dict[str, Any]:
metadata: dict[str, Any] = {
"run.kind": str(kind),
"run.algo": spec.algorithm.name,
"run.backend": spec.runtime.backend,
"run.device": spec.runtime.device,
"run.scenario": str(scenario),
"run.seed": spec.runtime.seed,
"run.tags": list(tags),
}
if group:
metadata["run.group"] = group
return metadata

View File

@@ -1,7 +1,6 @@
"""shared factor definitions for experimental designs"""
import numpy as np
from dataclasses import dataclass, field
from typing import Callable, Any
from dataclasses import dataclass
@dataclass
class Factor:

View File

@@ -1,5 +1,7 @@
"""full factorial design - all factor combinations"""
import sys
sys.path.insert(0, "..")
import logging
from itertools import product
@@ -12,6 +14,7 @@ from .factors import FACTORS, DEMAND_FUNCTIONS, SEEDS_PER_CONFIG
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
def generate_configs():
"""generate all factor combinations with seeds"""
all_levels = [f.levels for f in FACTORS]
@@ -22,10 +25,13 @@ def generate_configs():
base = {names[i]: combo[i] for i in range(len(names))}
for seed in range(SEEDS_PER_CONFIG):
cfg = {**base, "seed": seed}
cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
cfg["id"] = hashlib.md5(
json.dumps(cfg, sort_keys=True).encode()
).hexdigest()[:8]
configs.append(cfg)
return configs
def run_single(cfg: dict) -> dict:
"""execute one experiment config, return metrics"""
from engine.wrapper import PHANTOM
@@ -49,7 +55,8 @@ def run_single(cfg: dict) -> dict:
obs, reward, term, trunc, _ = env.step(action)
total_reward += reward
steps += 1
if term: break
if term:
break
env.close()
return {
@@ -60,22 +67,28 @@ def run_single(cfg: dict) -> dict:
"steps": steps,
}
def run_study(max_workers: int = None, output: str = "results_full.jsonl"):
configs = generate_configs()
log.info(f"full factorial: {len(configs)} configs ({len(configs)//SEEDS_PER_CONFIG} unique × {SEEDS_PER_CONFIG} seeds)")
log.info(
f"full factorial: {len(configs)} configs ({len(configs) // SEEDS_PER_CONFIG} unique × {SEEDS_PER_CONFIG} seeds)"
)
results = []
with ProcessPoolExecutor(max_workers=max_workers) as ex:
for i, result in enumerate(ex.map(run_single, configs)):
results.append(result)
if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}")
if (i + 1) % 100 == 0:
log.info(f"progress: {i + 1}/{len(configs)}")
Path(output).write_text("\n".join(json.dumps(r) for r in results))
log.info(f"wrote {len(results)} results to {output}")
return results
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--workers", type=int, default=None)
p.add_argument("--output", default="results_full.jsonl")
@@ -83,7 +96,9 @@ if __name__ == "__main__":
args = p.parse_args()
configs = generate_configs()
log.info(f"design: {len(configs)} runs | factors: {[f.name for f in FACTORS]} | levels: {[len(f.levels) for f in FACTORS]}")
log.info(
f"design: {len(configs)} runs | factors: {[f.name for f in FACTORS]} | levels: {[len(f.levels) for f in FACTORS]}"
)
if not args.dry_run:
run_study(args.workers, args.output)

View File

@@ -0,0 +1,136 @@
import sys
import numpy as np
import pandas as pd
from pathlib import Path
import matplotlib.pyplot as plt
from gymnasium.wrappers import FlattenObservation
from stable_baselines3 import PPO
# Add parent directory to path to allow importing engine
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from engine.wrapper import PHANTOM
from engine.lib.wrappers import EconomicMetricsWrapper
from engine.lib.providers import (
ProviderBenchmark,
BenchmarkConfig,
RandomBaseline,
SurgeBaseline,
)
def env_factory(alpha: float):
"""Creates a wrapped PHANTOM environment for testing at a specific alpha level."""
# Action levels=9 matches the trained PPO model
# n_products=8 matches the pretrained model's expectation of Box(16,)
env = PHANTOM(
n_products=8,
alpha=alpha,
N=100,
action_levels=9,
action_scale_low=0.8,
action_scale_high=1.2,
max_steps=20, # Short episodes so simulation goes fast
robust_points=1, # disable expensive adversarial lookaheads
render_mode=None,
)
env = EconomicMetricsWrapper(env)
return FlattenObservation(env)
def main():
print("Loading pre-trained Robust RL model...")
model_path = Path(__file__).parent.parent / "models" / "phantom_ppo.zip"
if not model_path.exists():
print(f"Error: Model not found at {model_path}")
print("Please ensure you have a trained model before running this script.")
return
rl_model = PPO.load(model_path)
# The action space is Discrete(9). Index 4 is the middle (1.0 scale).
n_actions = 9
mid_action = n_actions // 2
providers = {
"Static (Base)": lambda obs: mid_action,
"Random": RandomBaseline(n_actions),
"Heuristic Surge": SurgeBaseline(
n_actions, high_threshold=60.0, low_threshold=30.0
),
"Robust RL (PPO)": lambda obs: rl_model.predict(obs, deterministic=True)[0],
}
config = BenchmarkConfig(
n_episodes=10, # Lower episodes to run faster
alpha_range=[0.0, 0.5, 1.0], # Fewer alpha levels
baseline_name="Static (Base)",
)
print(f"\nStarting benchmark across alpha levels: {config.alpha_range}")
print(
f"Testing {len(providers)} strategies for {config.n_episodes} episodes each...\n"
)
benchmark = ProviderBenchmark(env_factory, providers, config)
results = benchmark.run()
# 1. Print tabular results
df = benchmark.to_dataframe()
summary = benchmark.summary_table()
print("\n--- Benchmark Summary Table ---")
print(summary)
# 2. Save results to CSV for thesis inclusion
out_dir = Path(__file__).parent / "results"
out_dir.mkdir(exist_ok=True)
csv_path = out_dir / "provider_comparison.csv"
df.to_csv(csv_path, index=False)
print(f"\nSaved raw results to {csv_path}")
# 3. Plot the degradation of COI / Revenue as alpha increases
plt.figure(figsize=(12, 5))
# Plot 1: Revenue vs Alpha
plt.subplot(1, 2, 1)
for name in providers.keys():
provider_data = df[df["name"] == name]
plt.plot(
provider_data["alpha"],
provider_data["mean_revenue"],
marker="o",
label=name,
linewidth=2,
)
plt.title("Revenue under Agent Contamination")
plt.xlabel("Contamination Level (α)")
plt.ylabel("Mean Episode Revenue ($)")
plt.grid(True, linestyle="--", alpha=0.7)
plt.legend()
# Plot 2: COI Preservation vs Alpha
plt.subplot(1, 2, 2)
for name in providers.keys():
provider_data = df[df["name"] == name]
plt.plot(
provider_data["alpha"],
provider_data["coi_preserved_pct"],
marker="s",
label=name,
linewidth=2,
)
plt.title("Cost of Information (COI) Preservation")
plt.xlabel("Contamination Level (α)")
plt.ylabel("COI Preserved (%)")
plt.grid(True, linestyle="--", alpha=0.7)
plt.legend()
plt.tight_layout()
plot_path = out_dir / "alpha_degradation_plot.png"
plt.savefig(plot_path, dpi=300)
print(f"Saved visualization to {plot_path}")
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,7 @@
"""mixed design: full factorial on primary factors, latin hypercube on secondary"""
import sys
sys.path.insert(0, "..")
import logging
from itertools import product
@@ -16,6 +18,7 @@ log = logging.getLogger(__name__)
LH_SAMPLES = 10
def generate_configs(lh_samples: int = LH_SAMPLES):
primary = [f for f in FACTORS if f.primary]
secondary = [f for f in FACTORS if not f.primary]
@@ -28,7 +31,9 @@ def generate_configs(lh_samples: int = LH_SAMPLES):
samples = lhs.random(n=lh_samples)
for s in samples:
sec_vals = {
secondary[i].name: secondary[i].levels[int(s[i] * len(secondary[i].levels))]
secondary[i].name: secondary[i].levels[
int(s[i] * len(secondary[i].levels))
]
for i in range(len(secondary))
}
base = {primary[i].name: p_combo[i] for i in range(len(primary))}
@@ -36,10 +41,13 @@ def generate_configs(lh_samples: int = LH_SAMPLES):
for seed in range(SEEDS_PER_CONFIG):
cfg = {**base, "seed": seed}
cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
cfg["id"] = hashlib.md5(
json.dumps(cfg, sort_keys=True).encode()
).hexdigest()[:8]
configs.append(cfg)
return configs
def run_single(cfg: dict) -> dict:
from engine.wrapper import PHANTOM
import numpy as np
@@ -62,7 +70,8 @@ def run_single(cfg: dict) -> dict:
obs, reward, term, trunc, _ = env.step(action)
total_reward += reward
steps += 1
if term: break
if term:
break
env.close()
return {
@@ -73,23 +82,33 @@ def run_single(cfg: dict) -> dict:
"steps": steps,
}
def run_study(max_workers: int = None, output: str = "results_mixed.jsonl", lh_samples: int = LH_SAMPLES):
def run_study(
max_workers: int = None,
output: str = "results_mixed.jsonl",
lh_samples: int = LH_SAMPLES,
):
configs = generate_configs(lh_samples)
n_primary_cells = int(np.prod([len(f.levels) for f in FACTORS if f.primary]))
log.info(f"mixed LH: {len(configs)} configs ({n_primary_cells} primary × {lh_samples} LH × {SEEDS_PER_CONFIG} seeds)")
log.info(
f"mixed LH: {len(configs)} configs ({n_primary_cells} primary × {lh_samples} LH × {SEEDS_PER_CONFIG} seeds)"
)
results = []
with ProcessPoolExecutor(max_workers=max_workers) as ex:
for i, result in enumerate(ex.map(run_single, configs)):
results.append(result)
if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}")
if (i + 1) % 100 == 0:
log.info(f"progress: {i + 1}/{len(configs)}")
Path(output).write_text("\n".join(json.dumps(r) for r in results))
log.info(f"wrote {len(results)} results to {output}")
return results
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--workers", type=int, default=None)
p.add_argument("--output", default="results_mixed.jsonl")
@@ -100,7 +119,9 @@ if __name__ == "__main__":
primary = [f for f in FACTORS if f.primary]
secondary = [f for f in FACTORS if not f.primary]
configs = generate_configs(args.lh_samples)
log.info(f"design: {len(configs)} runs | primary: {[f.name for f in primary]} | secondary (LH): {[f.name for f in secondary]}")
log.info(
f"design: {len(configs)} runs | primary: {[f.name for f in primary]} | secondary (LH): {[f.name for f in secondary]}"
)
if not args.dry_run:
run_study(args.workers, args.output, args.lh_samples)

View File

@@ -0,0 +1,84 @@
method: random
metric:
name: objective/score
goal: maximize
command:
- ${env}
- python
- -m
- engine.train
parameters:
algo:
values: [ppo, a2c, dqn, qtable]
total_timesteps:
values: [30000, 50000, 80000]
seed:
values: [13, 42, 77]
n_products:
values: [8, 10, 12]
alpha:
distribution: uniform
min: 0.1
max: 0.6
lambda_coi:
distribution: uniform
min: 0.05
max: 0.6
robust_radius:
distribution: uniform
min: 0.0
max: 0.3
robust_points:
values: [3, 5, 7]
info_value:
distribution: uniform
min: 0.5
max: 2.0
revenue_weight:
values: [0.005, 0.01, 0.02]
learning_rate:
distribution: log_uniform_values
min: 1.0e-5
max: 1.0e-3
gamma:
values: [0.97, 0.99, 0.995]
buffer_size:
values: [20000, 50000, 100000]
batch_size:
values: [128, 256, 512]
tau:
values: [0.002, 0.005, 0.01]
train_freq:
values: [1, 4, 8]
learning_starts:
values: [500, 1000, 3000]
n_steps:
values: [512, 1024, 2048]
n_epochs:
values: [5, 10, 20]
gae_lambda:
values: [0.9, 0.95, 0.98]
clip_range:
values: [0.1, 0.2, 0.3]
ent_coef:
values: [0.0, 0.005, 0.01]
target_update_interval:
values: [500, 1000, 2000]
exploration_fraction:
values: [0.1, 0.2, 0.3]
exploration_final_eps:
values: [0.01, 0.03, 0.05]
action_levels:
values: [7, 9, 11]
action_scale_low:
values: [0.75, 0.8, 0.85]
action_scale_high:
values: [1.15, 1.2, 1.25]
q_lr:
values: [0.03, 0.05, 0.1, 0.2]
eps_start:
value: 1.0
eps_end:
values: [0.02, 0.05, 0.1]
eps_decay:
values: [0.999, 0.9995, 0.9999]

View File

@@ -0,0 +1,85 @@
method: grid
metric:
name: objective/score
goal: maximize
run_cap: 4
command:
- ${env}
- python
- -m
- engine.train
parameters:
algo:
values: [ppo, a2c, dqn, qtable]
seed:
value: 42
total_timesteps:
value: 12000
eval_episodes:
value: 3
eval_freq:
value: 500
log_freq:
value: 100
revenue_weight:
value: 0.01
n_products:
value: 8
N:
value: 80
alpha:
value: 0.3
lambda_coi:
value: 0.2
robust_radius:
value: 0.0
robust_points:
value: 1
info_value:
value: 1.0
learning_rate:
value: 0.0003
gamma:
value: 0.99
buffer_size:
value: 20000
batch_size:
value: 128
tau:
value: 0.005
train_freq:
value: 1
learning_starts:
value: 500
n_steps:
value: 512
n_epochs:
value: 10
gae_lambda:
value: 0.95
clip_range:
value: 0.2
ent_coef:
value: 0.0
target_update_interval:
value: 500
exploration_fraction:
value: 0.2
exploration_final_eps:
value: 0.05
action_levels:
value: 7
action_scale_low:
value: 0.9
action_scale_high:
value: 1.1
q_lr:
value: 0.1
q_bins:
value: 6
eps_start:
value: 1.0
eps_end:
value: 0.05
eps_decay:
value: 0.9995

View File

@@ -0,0 +1,54 @@
method: bayes
metric:
name: objective/score
goal: maximize
command:
- ${env}
- python
- -m
- engine.train
parameters:
algo:
value: sac
total_timesteps:
values: [50000, 80000, 120000]
seed:
values: [13, 42, 77]
alpha:
distribution: uniform
min: 0.15
max: 0.55
n_products:
values: [8, 10, 12]
lambda_coi:
distribution: uniform
min: 0.05
max: 0.5
robust_radius:
distribution: uniform
min: 0.05
max: 0.3
robust_points:
values: [3, 5, 7]
info_value:
distribution: uniform
min: 0.5
max: 2.0
revenue_weight:
values: [0.005, 0.01, 0.02]
learning_rate:
distribution: log_uniform_values
min: 3.0e-5
max: 1.0e-3
gamma:
values: [0.98, 0.99, 0.995]
buffer_size:
values: [50000, 100000, 200000]
batch_size:
values: [128, 256, 512]
tau:
values: [0.002, 0.005, 0.01]
train_freq:
values: [1, 4, 8]
learning_starts:
values: [1000, 3000, 5000]

View File

@@ -0,0 +1,86 @@
method: random
metric:
name: objective/score
goal: maximize
command:
- ${env}
- python
- -m
- engine.train
parameters:
algo:
values: [ppo, a2c, dqn, qtable]
arch:
values: [tiny, small, medium]
activation:
values: [relu, tanh]
total_timesteps:
values: [8000, 12000, 20000]
seed:
values: [13, 42, 77]
n_products:
values: [6, 8, 10]
alpha:
distribution: uniform
min: 0.1
max: 0.5
lambda_coi:
distribution: uniform
min: 0.05
max: 0.4
robust_radius:
values: [0.0, 0.1, 0.2]
robust_points:
values: [3, 5]
info_value:
values: [0.75, 1.0, 1.5]
revenue_weight:
values: [0.005, 0.01, 0.02]
learning_rate:
distribution: log_uniform_values
min: 1.0e-5
max: 5.0e-4
gamma:
values: [0.98, 0.99]
buffer_size:
values: [10000, 30000, 50000]
batch_size:
values: [64, 128, 256]
tau:
values: [0.002, 0.005, 0.01]
train_freq:
values: [1, 4]
learning_starts:
values: [500, 1000, 2000]
n_steps:
values: [256, 512, 1024]
n_epochs:
values: [5, 10]
gae_lambda:
values: [0.9, 0.95]
clip_range:
values: [0.1, 0.2]
ent_coef:
values: [0.0, 0.005]
target_update_interval:
values: [500, 1000]
exploration_fraction:
values: [0.1, 0.2]
exploration_final_eps:
values: [0.02, 0.05]
action_levels:
values: [5, 7, 9]
action_scale_low:
values: [0.85, 0.9]
action_scale_high:
values: [1.1, 1.15]
q_lr:
values: [0.05, 0.1, 0.2]
q_bins:
values: [4, 6, 8]
eps_start:
value: 1.0
eps_end:
values: [0.02, 0.05]
eps_decay:
values: [0.999, 0.9995]

View File

@@ -0,0 +1,23 @@
from .metrics import canonicalize_metrics
from .wandb import (
current_config,
finish_run,
get_wandb_module,
init_run,
log_metrics,
run_agent,
update_run_config,
update_summary,
)
__all__ = [
"canonicalize_metrics",
"current_config",
"finish_run",
"get_wandb_module",
"init_run",
"log_metrics",
"run_agent",
"update_run_config",
"update_summary",
]

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
from typing import Any, Mapping
from ..spec import TrainSpec
_ALIASES = {
"train/reward": "train/reward_mean",
"train/revenue": "train/revenue_mean",
"train/dqn_loss": "train/loss",
"eval/reward": "eval/reward_mean",
"eval/revenue": "eval/revenue_mean",
"train/steps_per_second": "runtime/steps_per_second",
}
def _as_float(value: Any, default: float | None = None) -> float | None:
if value is None:
return default
try:
return float(value)
except (TypeError, ValueError):
return default
def canonicalize_metrics(raw: Mapping[str, Any], spec: TrainSpec) -> dict[str, Any]:
metrics: dict[str, Any] = {}
for key, value in raw.items():
canonical = _ALIASES.get(str(key), str(key))
if canonical in metrics and canonical != key:
continue
metrics[canonical] = value
metrics.setdefault("train/global_step", spec.runtime.total_timesteps)
eval_reward = (
_as_float(
metrics.get("eval/robust_reward_worst", metrics.get("eval/reward_mean")),
0.0,
)
or 0.0
)
metrics["objective/score"] = eval_reward
margin_mean = _as_float(metrics.get("eval/margin_mean"), None)
if margin_mean is not None:
metrics["objective/constraint_margin"] = margin_mean - spec.env.margin_floor
coi_level = _as_float(metrics.get("eval/coi_level_mean"), None)
metrics["objective/coi_preserved"] = 0.0 if coi_level is None else coi_level
metrics["study/alpha"] = spec.study.alpha
metrics["study/lambda_coi"] = spec.study.lambda_coi
metrics["study/robust_radius"] = spec.study.robust_radius
metrics["study/info_value"] = spec.study.info_value
metrics["runtime/backend"] = spec.runtime.backend
metrics["runtime/device"] = spec.runtime.device
metrics["runtime/seed"] = spec.runtime.seed
return metrics

98
engine/telemetry/wandb.py Normal file
View File

@@ -0,0 +1,98 @@
from __future__ import annotations
from typing import Any, Callable, Iterable, Mapping
def get_wandb_module():
try:
import wandb
return wandb
except ImportError:
return None
def _require_wandb():
wandb = get_wandb_module()
if wandb is None:
raise ImportError("wandb is required for this workflow")
return wandb
def init_run(
*,
mode: str,
project: str | None = None,
config: Mapping[str, Any] | None = None,
name: str | None = None,
tags: Iterable[str] | None = None,
group: str | None = None,
sweep_mode: bool = False,
):
wandb = _require_wandb()
kwargs: dict[str, Any] = {"mode": mode}
if group:
kwargs["group"] = group
if sweep_mode:
run = wandb.init(**kwargs)
if name and run is not None:
run.name = name
return run
init_kwargs = dict(kwargs)
init_kwargs["project"] = project
if config is not None:
init_kwargs["config"] = dict(config)
if name:
init_kwargs["name"] = name
if tags:
init_kwargs["tags"] = list(tags)
return wandb.init(**init_kwargs)
def finish_run() -> None:
wandb = get_wandb_module()
if wandb is not None and wandb.run is not None:
wandb.finish()
def current_config() -> dict[str, Any]:
wandb = get_wandb_module()
if wandb is None or wandb.run is None:
return {}
return {key: wandb.config[key] for key in wandb.config.keys()}
def update_run_config(config: Mapping[str, Any]) -> None:
wandb = get_wandb_module()
if wandb is None or wandb.run is None:
return
try:
wandb.config.update(dict(config), allow_val_change=True)
except TypeError:
wandb.config.update(dict(config))
def log_metrics(metrics: Mapping[str, Any], *, step: int) -> None:
wandb = get_wandb_module()
if wandb is None or wandb.run is None:
return
wandb.log(dict(metrics), step=step)
def update_summary(metrics: Mapping[str, Any]) -> None:
wandb = get_wandb_module()
if wandb is None or wandb.run is None:
return
for key, value in metrics.items():
wandb.run.summary[key] = value
def run_agent(
sweep_id: str,
fn: Callable[[], None],
*,
count: int | None = None,
) -> None:
wandb = _require_wandb()
wandb.agent(sweep_id, function=fn, count=count)

View File

@@ -1,45 +1,230 @@
from stable_baselines3 import SAC
from stable_baselines3.common.callbacks import EvalCallback, BaseCallback
from .wrapper import PHANTOM
from __future__ import annotations
import argparse
from typing import Any
from .logging_utils import configure_logging
from .orchestrators import run_benchmark_cli, run_sweep_agent, run_train_once
from .spec import TrainSpec
class RenderCallback(BaseCallback):
"""Renders environment on every step for live visualization."""
def __init__(self, env: PHANTOM):
super().__init__()
self.env = env
def _on_step(self) -> bool:
self.env.render()
return True
def _parse_tags(raw: str | None) -> list[str]:
if raw is None:
return []
return [piece.strip() for piece in str(raw).split(",") if piece.strip()]
env = PHANTOM(n_products=10, alpha=0.3, render_mode="human")
eval_env = PHANTOM(n_products=10, alpha=0.3, render_mode=None)
def _probe_run_kind(argv: list[str]) -> str:
probe = argparse.ArgumentParser(add_help=False)
probe.add_argument("--run-kind", choices=["train", "benchmark"])
probe.add_argument("--run-mode", choices=["train", "benchmark"])
args, _ = probe.parse_known_args(argv)
return str(args.run_kind or args.run_mode or "train")
model = SAC(
"MultiInputPolicy",
env,
verbose=1,
learning_rate=3e-4,
buffer_size=50000,
batch_size=256,
tau=0.005,
gamma=0.99,
)
render_cb = RenderCallback(env)
eval_cb = EvalCallback(eval_env, eval_freq=1000, n_eval_episodes=5, verbose=1)
def _strip_run_kind(argv: list[str]) -> list[str]:
stripped: list[str] = []
skip_next = False
for item in argv:
if skip_next:
skip_next = False
continue
if item in {"--run-kind", "--run-mode"}:
skip_next = True
continue
if item.startswith("--run-kind=") or item.startswith("--run-mode="):
continue
stripped.append(item)
return stripped
model.learn(total_timesteps=50000, callback=[render_cb, eval_cb])
model.save("phantom_sac")
# test trained policy
env = PHANTOM(n_products=10, alpha=0.3, render_mode="human")
obs, _ = env.reset()
for _ in range(100):
action, _ = model.predict(obs, deterministic=True)
obs, reward, term, trunc, _ = env.step(action)
env.render()
if term or trunc: break
env.close()
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="PHANTOM unified training entrypoint")
parser.add_argument("--run-kind", choices=["train", "benchmark"], default="train")
parser.add_argument("--run-mode", choices=["train", "benchmark"])
parser.add_argument("--project", default="capstone")
parser.add_argument("--scenario", default="default")
parser.add_argument("--group", type=str)
parser.add_argument("--tags", type=str)
parser.add_argument("--backend", choices=["auto", "sb3"], default="auto")
parser.add_argument("--algo", choices=["ppo", "a2c", "dqn", "qtable", "sac"])
parser.add_argument("--seed", type=int)
parser.add_argument("--total-timesteps", type=int)
parser.add_argument("--model-dir", type=str)
parser.add_argument("--log-freq", type=int)
parser.add_argument("--checkpoint-interval", type=int)
parser.add_argument("--device", type=str)
parser.add_argument("--alpha", type=float)
parser.add_argument("--N", type=int)
parser.add_argument("--n-products", type=int)
parser.add_argument("--lambda-coi", type=float)
parser.add_argument("--info-value", type=float)
parser.add_argument("--robust-radius", type=float)
parser.add_argument("--robust-points", type=int)
parser.add_argument("--robust-rollouts", type=int)
parser.add_argument("--no-robust", action="store_true")
parser.add_argument("--eta-ux", type=float)
parser.add_argument("--reward-profit-weight", type=float)
parser.add_argument("--revenue-weight", type=float)
parser.add_argument("--price-low", type=float)
parser.add_argument("--price-high", type=float)
parser.add_argument("--action-levels", type=int)
parser.add_argument("--action-scale-low", type=float)
parser.add_argument("--action-scale-high", type=float)
parser.add_argument("--max-steps", type=int)
parser.add_argument("--margin-floor", type=float)
parser.add_argument("--margin-floor-patience", type=int)
parser.add_argument("--learning-rate", type=float)
parser.add_argument("--gamma", type=float)
parser.add_argument("--buffer-size", type=int)
parser.add_argument("--batch-size", type=int)
parser.add_argument("--tau", type=float)
parser.add_argument("--train-freq", type=int)
parser.add_argument("--learning-starts", type=int)
parser.add_argument("--target-update-interval", type=int)
parser.add_argument("--exploration-fraction", type=float)
parser.add_argument("--exploration-final-eps", type=float)
parser.add_argument("--n-steps", type=int)
parser.add_argument("--n-epochs", type=int)
parser.add_argument("--gae-lambda", type=float)
parser.add_argument("--clip-range", type=float)
parser.add_argument("--ent-coef", type=float)
parser.add_argument("--q-lr", type=float)
parser.add_argument("--q-bins", type=int)
parser.add_argument("--eps-start", type=float)
parser.add_argument("--eps-end", type=float)
parser.add_argument("--eps-decay", type=float)
parser.add_argument("--arch", type=str)
parser.add_argument("--activation", type=str)
parser.add_argument("--vf-coef", type=float)
parser.add_argument("--max-grad-norm", type=float)
parser.add_argument("--eval-freq", type=int)
parser.add_argument("--eval-episodes", type=int)
parser.add_argument("--sweep-agent", action="store_true")
parser.add_argument("--sweep-id", type=str)
parser.add_argument("--count", type=int, default=0)
parser.add_argument("--offline", action="store_true")
parser.add_argument("--no-wandb", action="store_true")
return parser
def _overrides_from_args(args: argparse.Namespace) -> dict[str, Any]:
backend = None if args.backend == "auto" else args.backend
overrides = {
"project": args.project,
"backend": backend,
"algo": args.algo,
"seed": args.seed,
"total_timesteps": args.total_timesteps,
"model_dir": args.model_dir,
"log_freq": args.log_freq,
"checkpoint_interval": args.checkpoint_interval,
"device": args.device,
"alpha": args.alpha,
"N": args.N,
"n_products": args.n_products,
"lambda_coi": args.lambda_coi,
"info_value": args.info_value,
"robust_radius": args.robust_radius,
"robust_points": args.robust_points,
"robust_rollouts": args.robust_rollouts,
"no_robust": args.no_robust,
"eta_ux": args.eta_ux,
"reward_profit_weight": args.reward_profit_weight,
"revenue_weight": args.revenue_weight,
"price_low": args.price_low,
"price_high": args.price_high,
"action_levels": args.action_levels,
"action_scale_low": args.action_scale_low,
"action_scale_high": args.action_scale_high,
"max_steps": args.max_steps,
"margin_floor": args.margin_floor,
"margin_floor_patience": args.margin_floor_patience,
"learning_rate": args.learning_rate,
"gamma": args.gamma,
"buffer_size": args.buffer_size,
"batch_size": args.batch_size,
"tau": args.tau,
"train_freq": args.train_freq,
"learning_starts": args.learning_starts,
"target_update_interval": args.target_update_interval,
"exploration_fraction": args.exploration_fraction,
"exploration_final_eps": args.exploration_final_eps,
"n_steps": args.n_steps,
"n_epochs": args.n_epochs,
"gae_lambda": args.gae_lambda,
"clip_range": args.clip_range,
"ent_coef": args.ent_coef,
"q_lr": args.q_lr,
"q_bins": args.q_bins,
"eps_start": args.eps_start,
"eps_end": args.eps_end,
"eps_decay": args.eps_decay,
"arch": args.arch,
"activation": args.activation,
"vf_coef": args.vf_coef,
"max_grad_norm": args.max_grad_norm,
"eval_freq": args.eval_freq,
"eval_episodes": args.eval_episodes,
}
return {key: value for key, value in overrides.items() if value is not None}
def main(argv: list[str] | None = None) -> None:
import sys
configure_logging()
raw_args = list(sys.argv[1:] if argv is None else argv)
run_kind = _probe_run_kind(raw_args)
if run_kind == "benchmark":
run_benchmark_cli(_strip_run_kind(raw_args))
return
parser = _build_parser()
args, unknown = parser.parse_known_args(raw_args)
if unknown:
raise ValueError(f"Unknown arguments for training mode: {' '.join(unknown)}")
overrides = _overrides_from_args(args)
scenario = str(args.scenario)
group = args.group
extra_tags = tuple(_parse_tags(args.tags))
if args.sweep_agent:
run_sweep_agent(
project=args.project,
sweep_id=str(args.sweep_id or ""),
count=int(args.count),
offline=bool(args.offline),
no_wandb=bool(args.no_wandb),
base_overrides=overrides,
kind="sweep",
scenario=scenario,
group=group,
extra_tags=extra_tags,
)
return
spec = TrainSpec.from_flat(overrides)
run_train_once(
spec,
project=args.project,
offline=bool(args.offline),
no_wandb=bool(args.no_wandb),
kind="train",
scenario=scenario,
group=group,
extra_tags=extra_tags,
)
if __name__ == "__main__":
main()

40
engine/train_core.py Normal file
View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from .spec import TrainSpec
from .telemetry.metrics import canonicalize_metrics
@dataclass(frozen=True)
class TrainResult:
spec: TrainSpec
metrics: dict[str, Any]
artifacts: dict[str, str]
events: list[dict[str, Any]]
def run_train(spec: TrainSpec) -> TrainResult:
cfg = spec.to_flat_dict()
algo = spec.algorithm.name
if algo == "qtable":
from .backends.qtable import train_qtable
_, raw_metrics = train_qtable(cfg)
else:
from .backends.sb3 import train_sb3
_, raw_metrics = train_sb3(cfg)
events_raw = raw_metrics.pop("_train_events", [])
events = [evt for evt in events_raw if isinstance(evt, dict)]
metrics = canonicalize_metrics(raw_metrics, spec)
artifacts: dict[str, str] = {}
model_path = raw_metrics.get("model/path")
if isinstance(model_path, str):
artifacts["model/path"] = model_path
return TrainResult(spec=spec, metrics=metrics, artifacts=artifacts, events=events)

130
engine/wandb_checkpoint.py Normal file
View File

@@ -0,0 +1,130 @@
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any, Mapping
try:
import wandb
from wandb.errors import CommError
HAS_WANDB = True
except ImportError:
HAS_WANDB = False
wandb = None # type: ignore[assignment]
CommError = RuntimeError # type: ignore[assignment]
def _safe_value(value: Any) -> Any:
if isinstance(value, (str, int, float, bool)) or value is None:
return value
if isinstance(value, (list, tuple)):
return [_safe_value(v) for v in value]
if isinstance(value, dict):
return {str(k): _safe_value(value[k]) for k in sorted(value)}
return str(value)
def _safe_scope(scope: str | None) -> str:
raw = "manual" if scope in (None, "") else str(scope)
cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "-", raw).strip("-")
return cleaned or "manual"
def checkpoint_artifact_name(
cfg: Mapping[str, Any], *, backend: str, sweep_id: str | None = None
) -> str:
payload = {k: _safe_value(cfg[k]) for k in sorted(cfg)}
scope = _safe_scope(sweep_id)
canonical = json.dumps(
{"backend": backend, "scope": scope, "cfg": payload},
sort_keys=True,
separators=(",", ":"),
)
digest = hashlib.sha1(canonical.encode("utf-8")).hexdigest()[:14]
return f"phantom-{backend}-ckpt-{scope}-{digest}"[:128]
def _is_missing_artifact_error(exc: Exception) -> bool:
if isinstance(exc, CommError):
msg = str(exc).lower()
return "not found" in msg or "does not exist" in msg
return False
def download_latest_checkpoint(
artifact_name: str, *, file_name: str
) -> tuple[Path, dict[str, Any]] | None:
if not HAS_WANDB or wandb.run is None:
return None
try:
artifact = wandb.run.use_artifact(f"{artifact_name}:latest")
except Exception as exc:
if _is_missing_artifact_error(exc):
return None
raise
directory = Path(artifact.download())
checkpoint_path = directory / file_name
if not checkpoint_path.exists():
return None
metadata = dict(getattr(artifact, "metadata", {}) or {})
return checkpoint_path, metadata
def _aliases_from_metadata(metadata: dict[str, Any] | None) -> list[str]:
aliases = ["latest"]
if metadata is None:
return aliases
if "step" in metadata:
try:
aliases.append(f"step-{int(metadata['step'])}")
except (TypeError, ValueError):
pass
return aliases
def log_checkpoint_bytes(
artifact_name: str,
*,
file_name: str,
payload: bytes,
metadata: dict[str, Any] | None = None,
) -> bool:
if not HAS_WANDB or wandb.run is None:
return False
with TemporaryDirectory(prefix="phantom-ckpt-") as tmpdir:
path = Path(tmpdir) / file_name
path.write_bytes(payload)
artifact = wandb.Artifact(
name=artifact_name,
type="checkpoint",
metadata=metadata or {},
)
artifact.add_file(path.as_posix(), name=file_name)
wandb.log_artifact(artifact, aliases=_aliases_from_metadata(metadata))
return True
def log_checkpoint_file(
artifact_name: str,
*,
file_path: str | Path,
artifact_file_name: str,
metadata: dict[str, Any] | None = None,
) -> bool:
if not HAS_WANDB or wandb.run is None:
return False
src = Path(file_path)
if not src.exists():
return False
artifact = wandb.Artifact(
name=artifact_name,
type="checkpoint",
metadata=metadata or {},
)
artifact.add_file(src.as_posix(), name=artifact_file_name)
wandb.log_artifact(artifact, aliases=_aliases_from_metadata(metadata))
return True

View File

@@ -3,39 +3,120 @@ from gymnasium import spaces
import numpy as np
from .engine import Limbo, MarketEngine, PricingEngine
from .lib.render import DashboardRenderer
from .lib.coi import (
compute_uplift_coi,
extract_purchases,
compute_agent_probability,
)
from .lib.behavior import get_transition_models, trajectory_to_events
from .lib.wrappers import EconomicMetricsWrapper
class _ActionPricingEngine(PricingEngine):
def __init__(self, n_products: int, price_bounds: tuple):
self._prices = np.full(n_products, price_bounds[0], dtype=float)
def set_prices(self, prices: np.ndarray):
self._prices = np.asarray(prices, dtype=float)
def act(self, _):
return self._prices
class PHANTOM(gym.Env):
"""Gymnasium wrapper for the Limbo pricing-market simulation. Platform sets prices, market responds with demand."""
"""Gymnasium wrapper for Limbo pricing-market simulation implementing thesis COI framework
reward = R(p,d) - λ·COI_leak(p,τ') per thesis Section on DR-RL
COI_leak uses behavioral divergence to estimate agent probability f(τ')
robust inner step: min over alpha in Wasserstein interval around nominal alpha
actions are discrete global price-scale moves
"""
metadata = {"render_modes": ["human", "ansi"]}
def __init__(self,
n_products: int = 10,
alpha: float = 0.3,
N: int = 100,
price_bounds: tuple = (10.0, 150.0),
lambda_coi: float = 0.1,
render_mode: str = None):
def __init__(
self,
n_products: int = 10,
alpha: float = 0.3,
N: int = 100,
human_params: tuple = (50.0, 10.0),
agent_params: tuple = (45.0, 15.0),
noise_std: float = 1.0,
price_bounds: tuple = (10.0, 150.0),
lambda_coi: float = 0.1,
coi_window: int = 10,
robust_radius: float = 0.0,
robust_points: int = 5,
robust_rollouts: int = 1,
info_value: float = 1.0,
eta_ux: float = 0.5,
reward_profit_weight: float = 1.0,
action_levels: int = 9,
action_scale_low: float = 0.9,
action_scale_high: float = 1.1,
max_steps: int = 100,
margin_floor: float = 0.05,
margin_floor_patience: int = 5,
render_mode: str = None,
):
super().__init__()
self.n_products = n_products
self.price_bounds = price_bounds
self.lambda_coi = lambda_coi
self.coi_window = coi_window
self.max_steps = max(1, int(max_steps))
self.margin_floor = float(
margin_floor
) # terminate if avg margin stays below this for patience steps
self.margin_floor_patience = max(1, int(margin_floor_patience))
self.render_mode = render_mode
self.alpha = alpha
self.alpha = float(alpha)
self.nominal_alpha = float(alpha)
self.N = N
self.market = MarketEngine(alpha=alpha, N=N)
self._platform_stub = PricingEngine()
self._limbo = Limbo(self._platform_stub, self.market)
self.action_space = spaces.Box(
low=price_bounds[0], high=price_bounds[1],
shape=(n_products,), dtype=np.float32
self.human_params = human_params
self.agent_params = agent_params
self.robust_radius = max(0.0, float(robust_radius))
self.robust_points = max(1, int(robust_points))
self.robust_rollouts = max(1, int(robust_rollouts))
self.info_value = float(info_value)
self.eta_ux = float(eta_ux)
self.reward_profit_weight = float(reward_profit_weight)
self.action_levels = max(2, int(action_levels))
self._action_scales = np.linspace(
float(action_scale_low), float(action_scale_high), self.action_levels
)
self.market = MarketEngine(
alpha=alpha,
N=N,
human_params=human_params,
agent_params=agent_params,
noise_std=noise_std,
)
self._platform_stub = _ActionPricingEngine(n_products, price_bounds)
self._limbo = Limbo(self._platform_stub, self.market)
self._set_market_mix(self.nominal_alpha)
self.action_space = spaces.Discrete(self.action_levels)
self.observation_space = spaces.Dict(
{
"demand": spaces.Box(
low=0.0, high=100.0, shape=(n_products,), dtype=np.float32
),
"prices": spaces.Box(
low=price_bounds[0],
high=price_bounds[1],
shape=(n_products,),
dtype=np.float32,
),
"signals": spaces.Box(
low=np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32),
high=np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32),
shape=(4,),
dtype=np.float32,
),
}
)
self.observation_space = spaces.Dict({
"demand": spaces.Box(low=0.0, high=100.0, shape=(n_products,), dtype=np.float32),
"prices": spaces.Box(low=price_bounds[0], high=price_bounds[1], shape=(n_products,), dtype=np.float32),
})
self._prices = None
self._demand = None
@@ -44,41 +125,221 @@ class PHANTOM(gym.Env):
self._price_history = []
self._revenue_history = []
self._renderer = None
self._initial_episode_prices = None
self._trajectories = [] # session trajectories for agent prob calculation
self.baseline_prices = np.full(self.n_products, self.price_bounds[0])
self._low_margin_streak = 0 # consecutive steps below margin_floor
self._last_agent_prob = float(self.alpha)
self._last_alpha_adv = float(self.alpha)
# load behavioral models for agent probability estimation
try:
self._human_trans, self._agent_trans = get_transition_models()
except Exception:
# fallback if behavioral data unavailable
self._human_trans, self._agent_trans = None, None
def _get_obs(self) -> dict:
demand_arr = np.array([self._demand.get(i, 0.0) for i in range(self.n_products)], dtype=np.float32)
return {"demand": demand_arr, "prices": self._prices.astype(np.float32)}
demand_arr = np.array(
[self._demand.get(i, 0.0) for i in range(self.n_products)], dtype=np.float32
)
signals = np.array(
[
float(np.clip(self._last_agent_prob, 0.0, 1.0)),
float(np.clip(self._last_alpha_adv, 0.0, 1.0)),
float(np.clip(self.nominal_alpha, 0.0, 1.0)),
float(np.clip(self.robust_radius, 0.0, 1.0)),
],
dtype=np.float32,
)
return {
"demand": demand_arr,
"prices": self._prices.astype(np.float32),
"signals": signals,
}
def _compute_reward(self, prices: np.ndarray, demand: dict) -> float:
revenue = np.sum(prices * np.array([demand.get(i, 0.0) for i in range(self.n_products)]))
# TODO: implement supra-competitive price punishment
return float(revenue)
def _set_market_mix(self, alpha: float):
alpha = float(np.clip(alpha, 0.0, 1.0))
n_agents = int(self.N * alpha)
self.alpha = alpha
self.market.alpha = alpha
self.market.Nagents = n_agents
self.market.Nhumans = self.N - n_agents
def _decode_action(self, action) -> np.ndarray:
base = (
self._prices
if self._prices is not None
else np.full(self.n_products, self.price_bounds[0], dtype=float)
)
if np.isscalar(action):
idx = int(np.clip(int(action), 0, self.action_levels - 1))
return np.clip(base * self._action_scales[idx], *self.price_bounds)
a = np.asarray(action)
if a.size == 1:
idx = int(np.clip(int(a.reshape(-1)[0]), 0, self.action_levels - 1))
return np.clip(base * self._action_scales[idx], *self.price_bounds)
return np.clip(a.astype(float), *self.price_bounds)
def _compute_agent_prob(self, trajectories=None) -> float:
trajectories = (
self.market.last_trajectories if trajectories is None else trajectories
)
if not trajectories or self._human_trans is None or self._agent_trans is None:
return float(self.market.alpha)
probs = []
for traj in trajectories:
events = trajectory_to_events(traj)
if len(events) < 2:
continue
probs.append(
compute_agent_probability(events, self._human_trans, self._agent_trans)
)
return float(np.mean(probs)) if probs else float(self.market.alpha)
def _compute_reward(
self, prices: np.ndarray, demand: dict, agent_prob: float, trajectories: list
) -> tuple[float, dict]:
demand_arr = np.array(
[demand.get(i, 0.0) for i in range(self.n_products)], dtype=float
)
revenue = float(np.dot(prices, demand_arr))
floor_cost = float(np.dot(self.baseline_prices, demand_arr))
profit = revenue - floor_cost
purchases = extract_purchases(trajectories)
coi_mix = compute_uplift_coi(prices, purchases, self.baseline_prices)
coi_leakage = float(agent_prob * self.info_value)
info_budget = max(floor_cost, 1.0)
coi_penalty = self.lambda_coi * coi_leakage * info_budget
if len(self._price_history) > 0:
volatility = float(
np.mean(
np.abs(prices - self._price_history[-1])
/ np.maximum(self.baseline_prices, 1.0)
)
)
else:
volatility = 0.0
ux_penalty = self.eta_ux * info_budget * volatility
reward_revenue = self.reward_profit_weight * profit
reward = reward_revenue - coi_penalty - ux_penalty
return reward, {
"revenue": revenue,
"cost_floor": floor_cost,
"profit": profit,
"coi_mix": float(coi_mix),
"coi_base": 0.0,
"coi_leakage": coi_leakage,
"coi_penalty": coi_penalty,
"coi_info_budget": info_budget,
"ux_penalty": ux_penalty,
"volatility": volatility,
"reward_revenue": reward_revenue,
"reward_total": reward,
}
def _alpha_candidates(self) -> np.ndarray:
if self.robust_radius <= 0.0 or self.robust_points == 1:
return np.array([self.nominal_alpha], dtype=float)
lo = max(0.0, self.nominal_alpha - self.robust_radius)
hi = min(1.0, self.nominal_alpha + self.robust_radius)
return np.linspace(lo, hi, self.robust_points)
def _evaluate_candidate(self, alpha: float, prices: np.ndarray) -> float:
self._set_market_mix(alpha)
rewards = []
for _ in range(self.robust_rollouts):
demand = self.market.act(prices)
trajectories = list(self.market.last_trajectories)
agent_prob = self._compute_agent_prob(trajectories)
reward, _ = self._compute_reward(prices, demand, agent_prob, trajectories)
rewards.append(float(reward))
return float(np.mean(rewards)) if rewards else 0.0
def _select_adversarial_alpha(self, prices: np.ndarray) -> float:
"""inner robust step: evaluate candidates and pick worst-case alpha"""
candidates = self._alpha_candidates()
evaluations = [
(float(alpha), self._evaluate_candidate(float(alpha), prices))
for alpha in candidates
]
best_alpha, _ = min(evaluations, key=lambda x: x[1])
return best_alpha
def _record_history(self):
demand_arr = np.array([self._demand.get(i, 0.0) for i in range(self.n_products)])
demand_arr = np.array(
[self._demand.get(i, 0.0) for i in range(self.n_products)]
)
self._demand_history.append(demand_arr)
self._price_history.append(self._prices.copy())
self._revenue_history.append(np.sum(self._prices * demand_arr))
def reset(self, seed=None, options=None):
super().reset(seed=seed)
self._set_market_mix(self.nominal_alpha)
self._limbo.reset()
self._prices = np.random.uniform(*self.price_bounds, size=self.n_products)
self._demand = self.market.act(self._prices)
self._platform_stub.set_prices(self._prices)
self._limbo.step()
self._demand = self._limbo.step()
self._initial_episode_prices = self._prices.copy()
self._step_count = 0
self._low_margin_streak = 0
self._demand_history, self._price_history, self._revenue_history = [], [], []
self._trajectories = list(getattr(self.market, "last_trajectories", []))
self._last_agent_prob = float(self.nominal_alpha)
self._last_alpha_adv = float(self.nominal_alpha)
self._record_history()
return self._get_obs(), {}
def step(self, action: np.ndarray):
self._prices = np.clip(action, *self.price_bounds)
self._demand = self.market.act(self._prices)
def step(self, action):
self._prices = self._decode_action(action)
alpha_adv = self._select_adversarial_alpha(self._prices)
self._set_market_mix(alpha_adv)
self._platform_stub.set_prices(self._prices)
self._step_count += 1
self._demand = self.market.act(self._prices)
trajectories = list(self.market.last_trajectories)
agent_prob = self._compute_agent_prob(trajectories)
self._trajectories.extend(trajectories)
self._last_agent_prob = float(agent_prob)
self._last_alpha_adv = float(alpha_adv)
reward, metrics = self._compute_reward(
self._prices, self._demand, agent_prob, trajectories
)
self._record_history()
reward = self._compute_reward(self._prices, self._demand)
terminated = self._step_count >= 100
# soft early termination when margin collapses for too long
avg_margin = float(np.mean(self._prices) - self.price_bounds[0]) / max(
float(np.mean(self._prices)), 1e-6
)
if avg_margin < self.margin_floor:
self._low_margin_streak += 1
else:
self._low_margin_streak = 0
margin_collapsed = self._low_margin_streak >= self.margin_floor_patience
terminated = self._step_count >= self.max_steps or margin_collapsed
return self._get_obs(), reward, terminated, False, {"step": self._step_count}
info = {
"step": self._step_count,
"agent_prob": agent_prob,
"alpha_adv": float(alpha_adv),
"alpha_nominal": float(self.nominal_alpha),
"wasserstein_radius": float(self.robust_radius),
"robust_rollouts": int(self.robust_rollouts),
**metrics,
"raw_revenue": np.sum(
self._prices
* np.array([self._demand.get(i, 0.0) for i in range(self.n_products)])
),
}
return self._get_obs(), reward, terminated, False, info
def _compute_elasticity(self) -> np.ndarray:
"""point elasticity: e = (dQ/dP) * (P/Q) via finite differences, clipped to [-5, 5]"""
@@ -87,10 +348,16 @@ class PHANTOM(gym.Env):
p, q = np.array(self._price_history), np.array(self._demand_history)
dp, dq = np.diff(p, axis=0), np.diff(q, axis=0)
valid = np.abs(dp) > 0.5
with np.errstate(divide='ignore', invalid='ignore'):
elasticity = np.where(valid, (dq / dp) * (p[:-1] / np.maximum(q[:-1], 1.0)), 0.0)
with np.errstate(divide="ignore", invalid="ignore"):
elasticity = np.where(
valid, (dq / dp) * (p[:-1] / np.maximum(q[:-1], 1.0)), 0.0
)
elasticity = np.nan_to_num(np.clip(elasticity, -5.0, 5.0), nan=0.0)
return np.mean(elasticity, axis=0) if len(elasticity) > 0 else np.zeros(self.n_products)
return (
np.mean(elasticity, axis=0)
if len(elasticity) > 0
else np.zeros(self.n_products)
)
def render(self):
if self.render_mode == "human":
@@ -98,7 +365,9 @@ class PHANTOM(gym.Env):
self._renderer = DashboardRenderer()
self._renderer.render(self)
elif self.render_mode == "ansi":
return f"step={self._step_count}, prices={self._prices}, demand={self._demand}"
return (
f"step={self._step_count}, prices={self._prices}, demand={self._demand}"
)
return None
def close(self):
@@ -108,11 +377,44 @@ class PHANTOM(gym.Env):
if __name__ == "__main__":
env = PHANTOM(n_products=15, alpha=0.3, N=100, render_mode="human")
obs, _ = env.reset()
for step in range(100):
action = env.action_space.sample()
obs, reward, term, trunc, info = env.step(action)
env.render()
if term: break
import wandb
from .lib import MetricsCallback
class RandomPolicy:
"""Minimal SB3-compatible random policy for baseline testing."""
def __init__(self, env):
self.env = env
self.num_timesteps = 0
def learn(self, total_timesteps, callback=None):
callback.model = self
callback.num_timesteps = 0
callback.locals = {}
callback.on_training_start({}, {})
obs, _ = self.env.reset()
for step in range(total_timesteps):
action = self.env.action_space.sample()
obs, reward, term, trunc, info = self.env.step(action)
self.num_timesteps = step + 1
callback.num_timesteps = self.num_timesteps
callback.locals = {"infos": [info]}
callback.on_step()
if term or trunc:
callback.on_rollout_end()
obs, _ = self.env.reset()
return self
def predict(self, obs, **kwargs):
return self.env.action_space.sample(), None
wandb.init(project="capstone", config={"policy": "random", "alpha": 0.3})
env = EconomicMetricsWrapper(PHANTOM(n_products=15, alpha=0.3, render_mode=None))
model = RandomPolicy(env)
model.learn(total_timesteps=1000, callback=MetricsCallback())
print(f"Episode revenue: {env.episode_revenue:.1f}")
wandb.finish()
env.close()

View File

@@ -0,0 +1,269 @@
"""
Session-Aware Pricing DAG
THIS implements the core pricing computation (policy layer).
Flow: τ → θ̂ → D → p*
1. Fetch recent sessions from Kafka (last 10 active)
2. Extract features per session (τ → θ̂)
3. Map features to demand proxy (θ̂ → D)
4. Compute optimal prices (D → p*)
5. Write to Redis session:{sessionId}:prices
Scheduled: every 1 minute when enabled
"""
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.utils.dates import days_ago
from datetime import timedelta
import pandas as pd
import numpy as np
import logging
import sys
import pickle
sys.path.insert(0, '/opt/airflow')
from procesing.context import PipelineContext
from procesing.providers import SupabaseProvider, BackendAPIProvider
from procesing.steps.session import ExtractSessionFeaturesStep
from procesing.pricers.simple import SimpleSurgePricer, session_features_to_demand
from procesing.pricing import StateSpace
from lib.model_registry import ModelRegistry
DEFAULT_ARGS = {
'owner': 'phantom-research',
'depends_on_past': False,
'email_on_failure': False,
'email_on_retry': False,
'retries': 1,
'retry_delay': timedelta(seconds=30),
}
class CompositeProvider(SupabaseProvider, BackendAPIProvider):
def __init__(self):
SupabaseProvider.__init__(self)
BackendAPIProvider.__init__(self)
def _get_context(store_mode: str = 'hotel') -> PipelineContext:
return PipelineContext(provider=CompositeProvider(), store_mode=store_mode)
def fetch_recent_sessions(**kwargs):
"""
Task: Fetch last N active sessions from Kafka.
Returns: DataFrame of interaction events for recent sessions.
"""
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
store_mode = dag_conf.get('store_mode', 'hotel')
session_limit = dag_conf.get('session_limit', 10)
ctx = _get_context(store_mode)
provider = ctx.provider
# fetch all recent interactions from Kafka
try:
interactions_df = provider.fetch_kafka_topic("user-interactions")
except Exception as e:
logging.error(f"Failed to fetch interactions: {e}")
kwargs['ti'].xcom_push(key='sessions_data', value=pickle.dumps(pd.DataFrame()))
return 0
if interactions_df.empty or 'sessionId' not in interactions_df.columns:
kwargs['ti'].xcom_push(key='sessions_data', value=pickle.dumps(pd.DataFrame()))
return 0
# identify last N active sessions (most recent by event count)
recent_sessions = interactions_df['sessionId'].value_counts().head(session_limit).index.tolist()
# filter to only those sessions
filtered_df = interactions_df[interactions_df['sessionId'].isin(recent_sessions)].copy()
kwargs['ti'].xcom_push(key='sessions_data', value=pickle.dumps(filtered_df))
kwargs['ti'].xcom_push(key='session_ids', value=recent_sessions)
logging.info(f"Fetched {len(filtered_df)} events for {len(recent_sessions)} sessions")
return len(recent_sessions)
def extract_session_features(**kwargs):
"""
Task: Extract behavioral features from session trajectories.
THIS implements τ → θ̂ transformation.
"""
ti = kwargs['ti']
sessions_df = pickle.loads(ti.xcom_pull(key='sessions_data'))
if sessions_df.empty:
ti.xcom_push(key='session_features', value=pickle.dumps(pd.DataFrame()))
return 0
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
ctx = _get_context(dag_conf.get('store_mode', 'hotel'))
# extract features using vectorized pipeline
feature_extractor = ExtractSessionFeaturesStep(ctx)
features_df = feature_extractor.transform(sessions_df)
ti.xcom_push(key='session_features', value=pickle.dumps(features_df))
logging.info(f"Extracted {len(features_df.columns)} features for {len(features_df)} sessions")
logging.info(f"Feature columns: {list(features_df.columns)}")
logging.info(f"Sample features (first session):\n{features_df.iloc[0].to_dict()}")
return len(features_df)
def compute_session_prices(**kwargs):
"""
Task: Compute optimal prices for each session.
THIS implements θ̂ → D → p* transformation.
"""
ti = kwargs['ti']
features_df = pickle.loads(ti.xcom_pull(key='session_features'))
if features_df.empty:
ti.xcom_push(key='price_results', value=pickle.dumps({}))
return 0
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
store_mode = dag_conf.get('store_mode', 'hotel')
ctx = _get_context(store_mode)
# fetch product catalog for base prices
products_df = ctx.provider.fetch_products(store_mode)
if products_df.empty:
logging.error("No products found in catalog")
ti.xcom_push(key='price_results', value=pickle.dumps({}))
return 0
products_df['base_price'] = products_df['metadata'].apply(
lambda m: m.get('base_price', 100.0) if isinstance(m, dict) else 100.0
)
# initialize pricing model
pricer = SimpleSurgePricer(
high_threshold=dag_conf.get('high_threshold', 10),
low_threshold=dag_conf.get('low_threshold', 2),
surge_multiplier=dag_conf.get('surge_multiplier', 1.15),
discount_multiplier=dag_conf.get('discount_multiplier', 0.95)
)
pricer.fit(products_df)
# compute prices per session
price_results = {}
n_products = len(products_df)
logging.info(f"Starting price computation for {len(features_df)} sessions, {n_products} products")
logging.info(f"Pricer config: high_thresh={pricer.high_threshold}, low_thresh={pricer.low_threshold}, surge_mult={pricer.surge_multiplier}")
for idx, session_row in features_df.iterrows():
session_id = session_row.get('sessionId')
if not session_id:
continue
# map features to demand proxy (θ̂ → D)
session_features_single = pd.DataFrame([session_row])
demand_proxy = session_features_to_demand(session_features_single)
logging.info(f"[Session {session_id}] Features → Demand: {demand_proxy:.2f}")
logging.info(f"[Session {session_id}] Key features: velocity={session_row.get('interaction_velocity', 0):.2f}, cart_ratio={session_row.get('cart_to_view_ratio', 0):.2f}, item_views={session_row.get('item_views', 0)}")
# build state space
state_space = StateSpace(
demand=np.full(n_products, demand_proxy), # broadcast session demand to all products
prices=products_df['base_price'].values,
session_features=session_features_single
)
# compute optimal prices (D → p*)
optimal_prices = pricer.predict(state_space)
base_avg = products_df['base_price'].mean()
optimal_avg = optimal_prices.mean()
price_change_pct = ((optimal_avg - base_avg) / base_avg) * 100
logging.info(f"[Session {session_id}] Price adjustment: base_avg={base_avg:.2f}, optimal_avg={optimal_avg:.2f}, change={price_change_pct:+.1f}%")
# store as dict {productId: price}
price_map = {
str(products_df.iloc[i]['id']): float(optimal_prices[i])
for i in range(n_products)
}
price_results[session_id] = price_map
ti.xcom_push(key='price_results', value=pickle.dumps(price_results))
logging.info(f"Computed prices for {len(price_results)} sessions, {n_products} products each")
return len(price_results)
def publish_to_registry(**kwargs):
"""
Task: Write session prices to Redis registry.
THIS is the write path: prices → session:{sessionId}:prices
"""
ti = kwargs['ti']
price_results = pickle.loads(ti.xcom_pull(key='price_results'))
if not price_results:
logging.warning("No prices to publish")
return 0
registry = ModelRegistry()
ttl = kwargs.get('dag_run').conf.get('ttl', 1800) if kwargs.get('dag_run') and kwargs.get('dag_run').conf else 1800
published_count = 0
for session_id, price_map in price_results.items():
registry.set_session_prices(session_id, price_map, ttl=ttl)
published_count += 1
logging.info(f"Published prices for {published_count} sessions to registry (TTL={ttl}s)")
return {
'sessions_published': published_count,
'products_per_session': len(next(iter(price_results.values()))) if price_results else 0,
'status': 'success'
}
# DAG definition
with DAG(
'session_pricing_pipeline',
default_args=DEFAULT_ARGS,
description='Session-aware pricing: extract features → compute prices → publish to registry',
schedule_interval='*/1 * * * *', # every 1 minute
start_date=days_ago(1),
catchup=False,
max_active_runs=1,
tags=['pricing', 'session-aware', 'research', 'real-time'],
) as dag:
t_fetch_sessions = PythonOperator(
task_id='fetch_recent_sessions',
python_callable=fetch_recent_sessions,
provide_context=True,
)
t_extract_features = PythonOperator(
task_id='extract_session_features',
python_callable=extract_session_features,
provide_context=True,
)
t_compute_prices = PythonOperator(
task_id='compute_session_prices',
python_callable=compute_session_prices,
provide_context=True,
)
t_publish = PythonOperator(
task_id='publish_to_registry',
python_callable=publish_to_registry,
provide_context=True,
)
# linear dependency: fetch → extract → compute → publish
t_fetch_sessions >> t_extract_features >> t_compute_prices >> t_publish

View File

@@ -0,0 +1 @@
from .encoder import Window, extract_windows, build_windows, WindowDataset, PrototypeClassifier, train, loocv

View File

@@ -0,0 +1,210 @@
"""Contrastive encoder via trajectory windowing. Classification by prototype distance."""
import sys
sys.path.insert(0, "/home/velocitatem/Documents/Projects/PHANTOM/sim/rl/behavior_loader")
sys.path.insert(0, "/home/velocitatem/Documents/Projects/PHANTOM/experiments/ml")
from sim.rl.behavior_loader.loader import JointLoader, PayloadModel
from arch import TrajectoryEncoder, featurize_trajectory, nt_xent_loss
from typing import List, Dict, Tuple
from dataclasses import dataclass
from datetime import datetime
import numpy as np, torch, torch.nn.functional as F, random, optuna
from torch.utils.data import Dataset, DataLoader
from torch.optim import Adam
from torch.utils.tensorboard import SummaryWriter
RUNS = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/ml/runs"
AGENT_DIR = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/agents/collected_data/"
HUMAN_DIR = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/collected_data/"
@dataclass
class Window:
events: List[PayloadModel]
traj_id: str
label: int # 0=human, 1=agent
def extract_windows(events: List[PayloadModel], traj_id: str, label: int,
sizes: List[int] = [5, 10, 15], stride: int = 2) -> List[Window]:
"""Multi-scale overlapping windows from trajectory"""
n = len(events)
wins = [Window(events[i:i+s], traj_id, label) for s in sizes if n >= s for i in range(0, n-s+1, stride)]
if n >= 3: wins.append(Window(events, traj_id, label)) # full traj
return wins
def build_windows(data: Dict[str, List], sizes=[5,10,15], stride=2) -> List[Window]:
return [w for tid, evts in data.items()
for w in extract_windows(evts, tid, 0 if tid.startswith('human_') else 1, sizes, stride)]
class WindowDataset(Dataset):
"""Yields (anchor, positive) pairs from same class"""
def __init__(self, windows: List[Window], dim: int = 64):
self.wins, self.dim = windows, dim
self.by_label = {0: [i for i,w in enumerate(windows) if w.label==0],
1: [i for i,w in enumerate(windows) if w.label==1]}
self.by_traj = {}
for i, w in enumerate(windows): self.by_traj.setdefault(w.traj_id, []).append(i)
def __len__(self): return len(self.wins)
def _feat(self, evts): return featurize_trajectory(evts, None, self.dim)
def _aug(self, evts): # subsample 70-100%
if len(evts) < 4: return evts
k = max(3, int(len(evts) * random.uniform(0.7, 1.0)))
start = random.randint(0, len(evts) - k)
return evts[start:start+k]
def __getitem__(self, idx):
w = self.wins[idx]
pool = [i for i in self.by_label[w.label] if self.wins[i].traj_id != w.traj_id]
pos_idx = random.choice(pool) if pool else idx
a = torch.tensor(self._feat(self._aug(w.events)), dtype=torch.float32)
p = torch.tensor(self._feat(self._aug(self.wins[pos_idx].events)), dtype=torch.float32)
return a, p, w.label
class PrototypeClassifier:
"""Classify by distance to class centroids"""
def __init__(self, encoder: TrajectoryEncoder, device = 'cuda', dim=64):
self.enc, self.dev, self.dim = encoder, device, dim
self.centroids = {0: None, 1: None}
def fit(self, windows: List[Window]):
self.enc.eval()
embs = {0: [], 1: []}
with torch.no_grad():
for w in windows:
x = torch.tensor(featurize_trajectory(w.events, None, self.dim), dtype=torch.float32)
z = self.enc(x.unsqueeze(0).unsqueeze(1).to(self.dev))
embs[w.label].append(z)
self.centroids = {k: torch.cat(v).mean(0, keepdim=True) if v else None for k, v in embs.items()}
return self
def predict(self, events: List[PayloadModel]) -> Tuple[int, float, Dict]:
"""Returns (pred, confidence, debug). Confidence via softmax over -distances."""
self.enc.eval()
with torch.no_grad():
x = torch.tensor(featurize_trajectory(events, None, self.dim), dtype=torch.float32)
z = self.enc(x.unsqueeze(0).unsqueeze(1).to(self.dev))
dists = {k: torch.norm(z - c, dim=1).item() for k, c in self.centroids.items() if c is not None}
if not dists: return 0, 0.0, {'d': {}, 'p': [0.5, 0.5]}
pred = min(dists, key=dists.get)
d0, d1 = dists.get(0, 1e6), dists.get(1, 1e6) # softmax(-d) gives higher prob to closer centroid
probs = F.softmax(torch.tensor([[-d0, -d1]]), dim=1).squeeze()
return pred, probs[pred].item(), {'d': dists, 'p': probs.tolist()}
def train(epochs=200, lr=5e-4, batch=16, dim=64, emb=32, temp=0.5,
sizes=[5,10,15], stride=2, name=None, verbose=True):
data = JointLoader(HUMAN_DIR, AGENT_DIR).get_data()
wins = build_windows(data, sizes, stride)
if verbose: print(f"Windows: {len(wins)} ({sum(w.label==0 for w in wins)}h/{sum(w.label==1 for w in wins)}a)")
dev = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
enc = TrajectoryEncoder(dim, emb).to(dev)
opt = Adam(enc.parameters(), lr=lr)
loader = DataLoader(WindowDataset(wins, dim), batch_size=batch, shuffle=True, drop_last=True)
name = name or f"enc_{dim}_{emb}_{datetime.now():%Y%m%d_%H%M%S}"
writer = SummaryWriter(f"{RUNS}/encoder/{name}")
for ep in range(epochs):
enc.train()
total, n = 0.0, 0
for a, p, _ in loader:
loss = nt_xent_loss(enc(a.unsqueeze(1).to(dev)), enc(p.unsqueeze(1).to(dev)), temp)
opt.zero_grad(); loss.backward(); opt.step()
total += loss.item(); n += 1
avg = total / max(n, 1)
writer.add_scalar('loss-ntxent', avg, ep)
if verbose and (ep+1) % 20 == 0: print(f"Epoch {ep+1}: {avg:.4f}")
writer.close()
return enc, wins, dev
def loocv(epochs=100, lr=5e-4, dim=64, emb=32, temp=0.5, sizes=[5,10,15], stride=2, verbose=True):
"""Leave-one-trajectory-out CV"""
data = JointLoader(HUMAN_DIR, AGENT_DIR).get_data()
dev = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
results = []
for test_id in data:
train_data = {k: v for k, v in data.items() if k != test_id}
if not any(k.startswith('human_') for k in train_data) or not any(k.startswith('agent_') for k in train_data):
continue
wins = build_windows(train_data, sizes, stride)
enc = TrajectoryEncoder(dim, emb).to(dev)
opt = Adam(enc.parameters(), lr=lr)
loader = DataLoader(WindowDataset(wins, dim), batch_size=min(16, len(wins)//2 or 1),
shuffle=True, drop_last=len(wins)>2)
for _ in range(epochs):
enc.train()
for a, p, _ in loader:
loss = nt_xent_loss(enc(a.unsqueeze(1).to(dev)), enc(p.unsqueeze(1).to(dev)), temp)
opt.zero_grad(); loss.backward(); opt.step()
clf = PrototypeClassifier(enc, dev, dim).fit(wins)
pred, conf, dbg = clf.predict(data[test_id])
actual = 0 if test_id.startswith('human_') else 1
results.append((pred, actual, conf))
if verbose: print(f"{test_id[:18]}: pred={pred} conf={conf:.2f} actual={actual} {'OK' if pred==actual else 'MISS'}")
if results:
acc = sum(p==a for p,a,_ in results) / len(results)
if verbose: print(f"\nAccuracy: {acc:.1%} ({sum(p==a for p,a,_ in results)}/{len(results)})")
return acc, results
return 0.0, []
def hparam_tune(n_trials=50, epochs=60, n_jobs=2, verbose=True):
"""Optuna hyperparameter search maximizing LOOCV accuracy"""
def objective(trial):
lr = trial.suggest_float('lr', 1e-5, 1e-2, log=True)
dim = trial.suggest_categorical('dim', [32, 64, 128, 256])
emb = trial.suggest_categorical('emb', [16, 32, 64, 128])
temp = trial.suggest_float('temp', 0.05, 1.0)
stride = trial.suggest_int('stride', 1, 4)
sizes = [trial.suggest_int(f's{i}', 3, 20) for i in range(3)]
sizes = sorted(set(sizes)) # unique sorted
acc, _ = loocv(epochs, lr, dim, emb, temp, sizes, stride, verbose=False)
return acc
study = optuna.create_study(direction='maximize', study_name='encoder_hparam',
sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(objective, n_trials=n_trials, n_jobs=n_jobs, show_progress_bar=verbose)
best = study.best_params
if verbose:
print(f"\nBest accuracy: {study.best_value:.1%}")
print(f"Best params: {best}")
return best, study
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument('--mode', choices=['train', 'eval', 'hparam'], default='train')
p.add_argument('--epochs', type=int, default=200)
p.add_argument('--lr', type=float, default=5e-4)
p.add_argument('--dim', type=int, default=128)
p.add_argument('--emb', type=int, default=64)
p.add_argument('--temp', type=float, default=0.1)
p.add_argument('--sizes', type=str, default='5,10,15')
p.add_argument('--stride', type=int, default=2)
p.add_argument('--n_trials', type=int, default=50)
args = p.parse_args()
sizes = [int(x) for x in args.sizes.split(',')]
if args.mode == 'train':
enc, wins, dev = train(args.epochs, args.lr, 16, args.dim, args.emb, args.temp, sizes, args.stride)
elif args.mode == 'hparam':
best, study = hparam_tune(args.n_trials, min(args.epochs, 60))
else:
loocv(args.epochs, args.lr, args.dim, args.emb, args.temp, sizes, args.stride)

View File

@@ -0,0 +1,957 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 10,
"id": "62eafcd9-5462-4063-8873-0e7fb9add907",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from kafka import KafkaConsumer\n",
"import pandas as pd\n",
"import json\n",
"import numpy as np\n",
"import os\n",
"from dotenv import load_dotenv\n",
"import matplotlib.pyplot as plt\n",
"from IPython.display import display, SVG, Image\n",
"load_dotenv()"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "4af65cb4-e8cf-4877-b2db-13ac19f3838f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 73 entries, 0 to 72\n",
"Data columns (total 13 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 sessionId 73 non-null object \n",
" 1 eventName 73 non-null object \n",
" 2 page 73 non-null object \n",
" 3 productId 67 non-null object \n",
" 4 storeMode 73 non-null object \n",
" 5 userAgent 73 non-null object \n",
" 6 ts 73 non-null object \n",
" 7 metadata_referrer 6 non-null object \n",
" 8 metadata_roomType 45 non-null object \n",
" 9 metadata_price 45 non-null float64\n",
" 10 metadata_nights 45 non-null float64\n",
" 11 metadata_elementText 22 non-null object \n",
" 12 metadata_dwellTime 22 non-null float64\n",
"dtypes: float64(3), object(10)\n",
"memory usage: 7.5+ KB\n"
]
}
],
"source": [
"KAFKA_PORT=os.getenv(\"KAFKA_PORT\", 9092)\n",
"topic = \"user-interactions\"\n",
"consumer = KafkaConsumer(\n",
" topic, \n",
" enable_auto_commit=True,\n",
" value_deserializer=lambda x: json.loads(x.decode('utf-8')),\n",
" auto_offset_reset='earliest', \n",
" bootstrap_servers=['localhost:9092'])\n",
"messages=consumer.poll(timeout_ms=1000,max_records=10000)\n",
"df = []\n",
"for m in messages.values():\n",
" for i in m:\n",
" df.append(i.value)\n",
"df = pd.DataFrame(df)\n",
"# explode metadata col json\n",
"df = df.join(pd.json_normalize(df.pop(\"metadata\"), sep=\".\").add_prefix(\"metadata_\"))\n",
"df.info()"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "f6819a1c-32ab-49c7-845b-5df7bf60f561",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>sessionId</th>\n",
" <th>eventName</th>\n",
" <th>page</th>\n",
" <th>productId</th>\n",
" <th>storeMode</th>\n",
" <th>userAgent</th>\n",
" <th>ts</th>\n",
" <th>metadata_referrer</th>\n",
" <th>metadata_roomType</th>\n",
" <th>metadata_price</th>\n",
" <th>metadata_nights</th>\n",
" <th>metadata_elementText</th>\n",
" <th>metadata_dwellTime</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>d176d7c9-4027-4702-9e31-2a71395cdda0</td>\n",
" <td>page_view</td>\n",
" <td>/products</td>\n",
" <td>None</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53...</td>\n",
" <td>2025-11-14T13:23:46.270Z</td>\n",
" <td></td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>f0317a5d-e424-44e9-b784-c8f7291ffe31</td>\n",
" <td>page_view</td>\n",
" <td>/</td>\n",
" <td>None</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Geck...</td>\n",
" <td>2025-11-14T13:26:00.291Z</td>\n",
" <td></td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>f0317a5d-e424-44e9-b784-c8f7291ffe31</td>\n",
" <td>page_view</td>\n",
" <td>/products</td>\n",
" <td>None</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Geck...</td>\n",
" <td>2025-11-14T13:26:07.769Z</td>\n",
" <td></td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>f0317a5d-e424-44e9-b784-c8f7291ffe31</td>\n",
" <td>view_item_page</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Geck...</td>\n",
" <td>2025-11-14T13:26:15.010Z</td>\n",
" <td>NaN</td>\n",
" <td>Premium Room</td>\n",
" <td>269.0</td>\n",
" <td>1.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>238dc588-a7ab-4c0e-bccd-6abca5076c66</td>\n",
" <td>page_view</td>\n",
" <td>/products</td>\n",
" <td>None</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7...</td>\n",
" <td>2025-11-14T13:27:15.457Z</td>\n",
" <td></td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>238dc588-a7ab-4c0e-bccd-6abca5076c66</td>\n",
" <td>view_item_page</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7...</td>\n",
" <td>2025-11-14T13:27:15.591Z</td>\n",
" <td>NaN</td>\n",
" <td>Premium Room</td>\n",
" <td>264.0</td>\n",
" <td>2.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>432</th>\n",
" <td>214d9fad-9b00-40c3-bd0e-7739b6acd654</td>\n",
" <td>click</td>\n",
" <td>1762448192425</td>\n",
" <td>DIV</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>/</td>\n",
" <td>NaN</td>\n",
" <td>1623.0</td>\n",
" <td>493.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>238dc588-a7ab-4c0e-bccd-6abca5076c66</td>\n",
" <td>view_item_page</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7...</td>\n",
" <td>2025-11-14T13:27:21.483Z</td>\n",
" <td>NaN</td>\n",
" <td>Premium Room</td>\n",
" <td>264.0</td>\n",
" <td>2.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>238dc588-a7ab-4c0e-bccd-6abca5076c66</td>\n",
" <td>hover_over_title</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7...</td>\n",
" <td>2025-11-14T13:27:22.646Z</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>Grand Plaza Hotel</td>\n",
" <td>1200.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>238dc588-a7ab-4c0e-bccd-6abca5076c66</td>\n",
" <td>view_item_page</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7...</td>\n",
" <td>2025-11-14T13:27:25.889Z</td>\n",
" <td>NaN</td>\n",
" <td>Premium Room</td>\n",
" <td>264.0</td>\n",
" <td>2.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>35</th>\n",
" <td>013fc334-4045-4d5a-8739-dd0a8766a63b</td>\n",
" <td>page_view</td>\n",
" <td>/products</td>\n",
" <td>None</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53...</td>\n",
" <td>2025-11-14T13:53:59.993Z</td>\n",
" <td></td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>36</th>\n",
" <td>013fc334-4045-4d5a-8739-dd0a8766a63b</td>\n",
" <td>view_item_page</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53...</td>\n",
" <td>2025-11-14T13:54:10.705Z</td>\n",
" <td>NaN</td>\n",
" <td>Premium Room</td>\n",
" <td>223.0</td>\n",
" <td>3.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>37</th>\n",
" <td>013fc334-4045-4d5a-8739-dd0a8766a63b</td>\n",
" <td>hover_over_title</td>\n",
" <td>/products</td>\n",
" <td>htl-0</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53...</td>\n",
" <td>2025-11-14T13:54:11.771Z</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>416.0</td>\n",
" <td>397.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>Grand Plaza Hotel</td>\n",
" <td>1200.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>38</th>\n",
" <td>013fc334-4045-4d5a-8739-dd0a8766a63b</td>\n",
" <td>view_item_page</td>\n",
" <td>/products</td>\n",
" <td>htl-1</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53...</td>\n",
" <td>2025-11-14T13:54:29.772Z</td>\n",
" <td>NaN</td>\n",
" <td>Standard Room</td>\n",
" <td>267.0</td>\n",
" <td>5.0</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>39</th>\n",
" <td>013fc334-4045-4d5a-8739-dd0a8766a63b</td>\n",
" <td>hover_over_title</td>\n",
" <td>/products</td>\n",
" <td>htl-1</td>\n",
" <td>hotel</td>\n",
" <td>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53...</td>\n",
" <td>2025-11-14T13:54:30.833Z</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>Seaside Resort</td>\n",
" <td>1200.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" sessionId eventName page \\\n",
"0 d176d7c9-4027-4702-9e31-2a71395cdda0 page_view /products \n",
"1 f0317a5d-e424-44e9-b784-c8f7291ffe31 page_view / \n",
"2 f0317a5d-e424-44e9-b784-c8f7291ffe31 page_view /products \n",
"3 f0317a5d-e424-44e9-b784-c8f7291ffe31 view_item_page /products \n",
"4 238dc588-a7ab-4c0e-bccd-6abca5076c66 page_view /products \n",
"5 238dc588-a7ab-4c0e-bccd-6abca5076c66 view_item_page /products \n",
"6 238dc588-a7ab-4c0e-bccd-6abca5076c66 view_item_page /products \n",
"7 238dc588-a7ab-4c0e-bccd-6abca5076c66 hover_over_title /products \n",
"8 238dc588-a7ab-4c0e-bccd-6abca5076c66 view_item_page /products \n",
"35 013fc334-4045-4d5a-8739-dd0a8766a63b page_view /products \n",
"36 013fc334-4045-4d5a-8739-dd0a8766a63b view_item_page /products \n",
"37 013fc334-4045-4d5a-8739-dd0a8766a63b hover_over_title /products \n",
"38 013fc334-4045-4d5a-8739-dd0a8766a63b view_item_page /products \n",
"39 013fc334-4045-4d5a-8739-dd0a8766a63b hover_over_title /products \n",
"\n",
" productId storeMode userAgent \\\n",
"0 None hotel Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53... \n",
"1 None hotel Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Geck... \n",
"2 None hotel Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Geck... \n",
"3 htl-0 hotel Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Geck... \n",
"4 None hotel Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7... \n",
"5 htl-0 hotel Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7... \n",
"6 htl-0 hotel Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7... \n",
"7 htl-0 hotel Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7... \n",
"8 htl-0 hotel Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7... \n",
"35 None hotel Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53... \n",
"36 htl-0 hotel Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53... \n",
"37 htl-0 hotel Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53... \n",
"38 htl-1 hotel Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53... \n",
"39 htl-1 hotel Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53... \n",
"\n",
" ts metadata_referrer metadata_roomType \\\n",
"0 2025-11-14T13:23:46.270Z NaN \n",
"1 2025-11-14T13:26:00.291Z NaN \n",
"2 2025-11-14T13:26:07.769Z NaN \n",
"3 2025-11-14T13:26:15.010Z NaN Premium Room \n",
"4 2025-11-14T13:27:15.457Z NaN \n",
"5 2025-11-14T13:27:15.591Z NaN Premium Room \n",
"6 2025-11-14T13:27:21.483Z NaN Premium Room \n",
"7 2025-11-14T13:27:22.646Z NaN NaN \n",
"8 2025-11-14T13:27:25.889Z NaN Premium Room \n",
"35 2025-11-14T13:53:59.993Z NaN \n",
"36 2025-11-14T13:54:10.705Z NaN Premium Room \n",
"37 2025-11-14T13:54:11.771Z NaN NaN \n",
"38 2025-11-14T13:54:29.772Z NaN Standard Room \n",
"39 2025-11-14T13:54:30.833Z NaN NaN \n",
"\n",
" metadata_price metadata_nights metadata_elementText metadata_dwellTime \n",
"0 NaN NaN NaN NaN \n",
"1 NaN NaN NaN NaN \n",
"2 NaN NaN NaN NaN \n",
"3 269.0 1.0 NaN NaN \n",
"4 NaN NaN NaN NaN \n",
"5 264.0 2.0 NaN NaN \n",
"6 264.0 2.0 NaN NaN \n",
"7 NaN NaN Grand Plaza Hotel 1200.0 \n",
"8 264.0 2.0 NaN NaN \n",
"35 NaN NaN NaN NaN \n",
"36 223.0 3.0 NaN NaN \n",
"37 NaN NaN Grand Plaza Hotel 1200.0 \n",
"38 267.0 5.0 NaN NaN \n",
"39 NaN NaN Seaside Resort 1200.0 "
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df.groupby('sessionId').head()"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "380eca5f-8304-4fb2-be32-e8bcfd312085",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['013fc334-4045-4d5a-8739-dd0a8766a63b',\n",
" '238dc588-a7ab-4c0e-bccd-6abca5076c66',\n",
" 'd176d7c9-4027-4702-9e31-2a71395cdda0',\n",
" 'f0317a5d-e424-44e9-b784-c8f7291ffe31']"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sessions = list(set(df['sessionId'])); sessions # 238dc588-a7ab-4c0e-bccd-6abca5076c66"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "f4ae6f81-dcb8-44be-aee7-30dbc3a6bae1",
"metadata": {},
"outputs": [],
"source": [
"# map sessions to experiments"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "050d90a4-20a9-47f5-b998-c31178a54cb3",
"metadata": {},
"outputs": [],
"source": [
"def build_transition_prob_matrix(df: pd.DataFrame):\n",
" df = df.dropna(subset=['eventName'])\n",
" events = df['eventName'].tolist()\n",
" labels = pd.Index(events).unique().tolist()\n",
" idx = {e:i for i,e in enumerate(labels)}\n",
" M = np.zeros((len(labels), len(labels)), dtype=float)\n",
" for a, b in zip(events, events[1:]):\n",
" M[idx[a], idx[b]] += 1\n",
" row_sums = M.sum(axis=1, keepdims=True)\n",
" with np.errstate(divide='ignore', invalid='ignore'):\n",
" P = np.divide(M, row_sums, where=row_sums>0) # row-normalized\n",
" return P, labels"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "e68f9004-82f5-4826-aece-e3dc6e15a18f",
"metadata": {},
"outputs": [],
"source": [
"# https://medium.com/data-science/time-series-data-markov-transition-matrices-7060771e362b\n",
"from graphviz import Digraph\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"def _as_prob_df(matrix, labels=None):\n",
" \"\"\"Return a square DataFrame with index=columns=labels.\"\"\"\n",
" if isinstance(matrix, pd.DataFrame):\n",
" # Ensure square and aligned\n",
" assert (matrix.index == matrix.columns).all(), \"Index/columns must match.\"\n",
" return matrix\n",
" matrix = np.asarray(matrix, dtype=float)\n",
" assert matrix.shape[0] == matrix.shape[1], \"Matrix must be square.\"\n",
" if labels is None:\n",
" raise ValueError(\"labels are required when matrix is not a DataFrame\")\n",
" assert len(labels) == matrix.shape[0], \"labels length must match matrix size.\"\n",
" return pd.DataFrame(matrix, index=list(labels), columns=list(labels))\n",
"\n",
"def _df_to_edgelist(P: pd.DataFrame, threshold=0.0, round_digits=2):\n",
" \"\"\"Build weighted edges > threshold.\"\"\"\n",
" edges = []\n",
" for src in P.index:\n",
" for dst in P.columns:\n",
" w = float(P.loc[src, dst])\n",
" if w > threshold:\n",
" edges.append((str(src), str(dst), f\"{w:.{round_digits}f}\"))\n",
" return edges\n",
"\n",
"def render_graph(fname, matrix, ls_index=None, threshold=0.0, fmt=\"svg\", view=False):\n",
" \"\"\"\n",
" fname: output file stem (no extension)\n",
" matrix: NumPy array or pandas DataFrame of transition PROBABILITIES\n",
" ls_index: ordered labels (required if matrix is not a DataFrame)\n",
" threshold: hide edges with weight <= threshold\n",
" fmt: 'svg'|'png'|'pdf' etc.\n",
" view: open after rendering\n",
" \"\"\"\n",
" P = _as_prob_df(matrix, labels=ls_index)\n",
" edges = _df_to_edgelist(P, threshold=threshold)\n",
"\n",
" g = Digraph(format=fmt)\n",
" g.attr(rankdir=\"LR\", size=\"30\")\n",
" g.attr(\"node\", shape=\"circle\")\n",
"\n",
" # ensure isolated nodes appear\n",
" for node in P.index:\n",
" g.node(str(node), width=\"1\", height=\"1\")\n",
"\n",
" for src, dst, label in edges:\n",
" g.edge(src, dst, label=label)\n",
"\n",
" g.render(fname, view=view, cleanup=True)\n",
" return g\n"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "e255a2c1-6454-4e5e-89f6-ef8ac51ab6cc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"013fc334-4045-4d5a-8739-dd0a8766a63b\n"
]
},
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 13.1.2 (0)\n",
" -->\n",
"<!-- Pages: 1 -->\n",
"<svg width=\"565pt\" height=\"354pt\"\n",
" viewBox=\"0.00 0.00 565.00 354.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 349.64)\">\n",
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-349.64 561.05,-349.64 561.05,4 -4,4\"/>\n",
"<!-- page_view -->\n",
"<g id=\"node1\" class=\"node\">\n",
"<title>page_view</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"48.19\" cy=\"-235.83\" rx=\"48.19\" ry=\"48.19\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"48.19\" y=\"-231.16\" font-family=\"Times,serif\" font-size=\"14.00\">page_view</text>\n",
"</g>\n",
"<!-- view_item_page -->\n",
"<g id=\"node2\" class=\"node\">\n",
"<title>view_item_page</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"232.88\" cy=\"-235.83\" rx=\"69.01\" ry=\"69.01\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"232.88\" y=\"-231.16\" font-family=\"Times,serif\" font-size=\"14.00\">view_item_page</text>\n",
"</g>\n",
"<!-- page_view&#45;&gt;view_item_page -->\n",
"<g id=\"edge1\" class=\"edge\">\n",
"<title>page_view&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M96.71,-235.83C113.69,-235.83 133.31,-235.83 152.25,-235.83\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"152.1,-239.33 162.1,-235.83 152.1,-232.33 152.1,-239.33\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"130.12\" y=\"-239.78\" font-family=\"Times,serif\" font-size=\"14.00\">1.00</text>\n",
"</g>\n",
"<!-- view_item_page&#45;&gt;view_item_page -->\n",
"<g id=\"edge2\" class=\"edge\">\n",
"<title>view_item_page&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M214.74,-302.59C217.1,-314.51 223.14,-322.84 232.88,-322.84 239.27,-322.84 244.07,-319.26 247.28,-313.42\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"250.57,-314.62 250.52,-304.02 243.95,-312.33 250.57,-314.62\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"232.88\" y=\"-326.79\" font-family=\"Times,serif\" font-size=\"14.00\">0.68</text>\n",
"</g>\n",
"<!-- hover_over_title -->\n",
"<g id=\"node3\" class=\"node\">\n",
"<title>hover_over_title</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"463.22\" cy=\"-275.83\" rx=\"69.81\" ry=\"69.81\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"463.22\" y=\"-271.16\" font-family=\"Times,serif\" font-size=\"14.00\">hover_over_title</text>\n",
"</g>\n",
"<!-- view_item_page&#45;&gt;hover_over_title -->\n",
"<g id=\"edge3\" class=\"edge\">\n",
"<title>view_item_page&#45;&gt;hover_over_title</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M300.48,-250.14C307.03,-251.43 313.58,-252.69 319.89,-253.83 340.12,-257.51 362.05,-261.1 382.5,-264.27\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"381.77,-267.7 392.19,-265.76 382.83,-260.78 381.77,-267.7\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-263.17\" font-family=\"Times,serif\" font-size=\"14.00\">0.29</text>\n",
"</g>\n",
"<!-- hover_over_paragraph -->\n",
"<g id=\"node4\" class=\"node\">\n",
"<title>hover_over_paragraph</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"463.22\" cy=\"-93.83\" rx=\"93.83\" ry=\"93.83\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"463.22\" y=\"-89.16\" font-family=\"Times,serif\" font-size=\"14.00\">hover_over_paragraph</text>\n",
"</g>\n",
"<!-- view_item_page&#45;&gt;hover_over_paragraph -->\n",
"<g id=\"edge4\" class=\"edge\">\n",
"<title>view_item_page&#45;&gt;hover_over_paragraph</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M292.09,-199.63C316.79,-184.27 346.14,-166.02 373.44,-149.04\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"375.08,-152.15 381.72,-143.89 371.38,-146.2 375.08,-152.15\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-185.68\" font-family=\"Times,serif\" font-size=\"14.00\">0.04</text>\n",
"</g>\n",
"<!-- hover_over_title&#45;&gt;view_item_page -->\n",
"<g id=\"edge5\" class=\"edge\">\n",
"<title>hover_over_title&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M399.53,-246.73C384.12,-240.88 367.42,-235.6 351.39,-232.58 339.13,-230.28 326.03,-229.26 313.19,-229.04\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"313.51,-225.54 303.51,-229.04 313.51,-232.54 313.51,-225.54\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-236.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.00</text>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.graphs.Digraph at 0x7f0779e818b0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"[]\n"
]
},
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 13.1.2 (0)\n",
" -->\n",
"<!-- Pages: 1 -->\n",
"<svg width=\"8pt\" height=\"8pt\"\n",
" viewBox=\"0.00 0.00 8.00 8.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 4)\">\n",
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-4 4,-4 4,4 -4,4\"/>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.graphs.Digraph at 0x7f6800fac980>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[0.00000000e+000 1.00000000e+000 0.00000000e+000 0.00000000e+000]\n",
" [0.00000000e+000 6.78571429e-001 2.85714286e-001 3.57142857e-002]\n",
" [0.00000000e+000 1.00000000e+000 0.00000000e+000 0.00000000e+000]\n",
" [2.05833592e-312 2.29175545e-312 4.94065646e-324 6.92110218e-310]]\n",
"238dc588-a7ab-4c0e-bccd-6abca5076c66\n"
]
},
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 13.1.2 (0)\n",
" -->\n",
"<!-- Pages: 1 -->\n",
"<svg width=\"565pt\" height=\"354pt\"\n",
" viewBox=\"0.00 0.00 565.00 354.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 349.64)\">\n",
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-349.64 561.05,-349.64 561.05,4 -4,4\"/>\n",
"<!-- page_view -->\n",
"<g id=\"node1\" class=\"node\">\n",
"<title>page_view</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"48.19\" cy=\"-109.83\" rx=\"48.19\" ry=\"48.19\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"48.19\" y=\"-105.16\" font-family=\"Times,serif\" font-size=\"14.00\">page_view</text>\n",
"</g>\n",
"<!-- view_item_page -->\n",
"<g id=\"node2\" class=\"node\">\n",
"<title>view_item_page</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"232.88\" cy=\"-197.83\" rx=\"69.01\" ry=\"69.01\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"232.88\" y=\"-193.16\" font-family=\"Times,serif\" font-size=\"14.00\">view_item_page</text>\n",
"</g>\n",
"<!-- page_view&#45;&gt;view_item_page -->\n",
"<g id=\"edge1\" class=\"edge\">\n",
"<title>page_view&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M92.02,-130.47C112.32,-140.25 137.13,-152.2 160.18,-163.3\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"158.39,-166.32 168.92,-167.51 161.43,-160.02 158.39,-166.32\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"130.12\" y=\"-157.78\" font-family=\"Times,serif\" font-size=\"14.00\">1.00</text>\n",
"</g>\n",
"<!-- view_item_page&#45;&gt;view_item_page -->\n",
"<g id=\"edge2\" class=\"edge\">\n",
"<title>view_item_page&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M214.74,-264.59C217.1,-276.51 223.14,-284.84 232.88,-284.84 239.27,-284.84 244.07,-281.26 247.28,-275.42\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"250.57,-276.62 250.52,-266.02 243.95,-274.33 250.57,-276.62\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"232.88\" y=\"-288.79\" font-family=\"Times,serif\" font-size=\"14.00\">0.19</text>\n",
"</g>\n",
"<!-- hover_over_title -->\n",
"<g id=\"node3\" class=\"node\">\n",
"<title>hover_over_title</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"463.22\" cy=\"-275.83\" rx=\"69.81\" ry=\"69.81\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"463.22\" y=\"-271.16\" font-family=\"Times,serif\" font-size=\"14.00\">hover_over_title</text>\n",
"</g>\n",
"<!-- view_item_page&#45;&gt;hover_over_title -->\n",
"<g id=\"edge3\" class=\"edge\">\n",
"<title>view_item_page&#45;&gt;hover_over_title</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M289.6,-237.16C299.36,-242.77 309.67,-247.94 319.89,-251.83 339.45,-259.28 361.4,-264.43 382.1,-267.98\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"381.52,-271.43 391.95,-269.55 382.62,-264.52 381.52,-271.43\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-265.16\" font-family=\"Times,serif\" font-size=\"14.00\">0.38</text>\n",
"</g>\n",
"<!-- hover_over_paragraph -->\n",
"<g id=\"node4\" class=\"node\">\n",
"<title>hover_over_paragraph</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"463.22\" cy=\"-93.83\" rx=\"93.83\" ry=\"93.83\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"463.22\" y=\"-89.16\" font-family=\"Times,serif\" font-size=\"14.00\">hover_over_paragraph</text>\n",
"</g>\n",
"<!-- view_item_page&#45;&gt;hover_over_paragraph -->\n",
"<g id=\"edge4\" class=\"edge\">\n",
"<title>view_item_page&#45;&gt;hover_over_paragraph</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M300.22,-180.71C317.22,-175.46 335.24,-169.12 351.39,-161.83 358.97,-158.41 366.67,-154.57 374.29,-150.49\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"375.84,-153.63 382.92,-145.75 372.47,-147.5 375.84,-153.63\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-178.15\" font-family=\"Times,serif\" font-size=\"14.00\">0.44</text>\n",
"</g>\n",
"<!-- hover_over_title&#45;&gt;view_item_page -->\n",
"<g id=\"edge5\" class=\"edge\">\n",
"<title>hover_over_title&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M398.52,-248.36C383.21,-242.16 366.82,-235.87 351.39,-230.58 338.42,-226.15 324.5,-221.86 310.94,-217.93\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"312.2,-214.65 301.62,-215.28 310.28,-221.39 312.2,-214.65\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-234.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.00</text>\n",
"</g>\n",
"<!-- hover_over_paragraph&#45;&gt;page_view -->\n",
"<g id=\"edge6\" class=\"edge\">\n",
"<title>hover_over_paragraph&#45;&gt;page_view</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M369.13,-95.76C310.26,-97.17 232.59,-99.41 163.87,-102.58 145.72,-103.42 125.98,-104.58 108.06,-105.73\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"107.86,-102.24 98.1,-106.38 108.31,-109.22 107.86,-102.24\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"232.88\" y=\"-106.53\" font-family=\"Times,serif\" font-size=\"14.00\">0.14</text>\n",
"</g>\n",
"<!-- hover_over_paragraph&#45;&gt;view_item_page -->\n",
"<g id=\"edge7\" class=\"edge\">\n",
"<title>hover_over_paragraph&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M372.68,-119.15C354.84,-125.32 336.5,-132.51 319.89,-140.58 312.9,-143.98 305.81,-147.87 298.86,-151.98\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"297.49,-148.71 290.78,-156.91 301.14,-154.69 297.49,-148.71\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"335.64\" y=\"-144.53\" font-family=\"Times,serif\" font-size=\"14.00\">0.86</text>\n",
"</g>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.graphs.Digraph at 0x7f6800f97110>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[0. 1. 0. 0. ]\n",
" [0. 0.1875 0.375 0.4375 ]\n",
" [0. 1. 0. 0. ]\n",
" [0.14285714 0.85714286 0. 0. ]]\n",
"d176d7c9-4027-4702-9e31-2a71395cdda0\n"
]
},
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 13.1.2 (0)\n",
" -->\n",
"<!-- Pages: 1 -->\n",
"<svg width=\"104pt\" height=\"104pt\"\n",
" viewBox=\"0.00 0.00 104.00 104.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 100.37)\">\n",
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-100.37 100.37,-100.37 100.37,4 -4,4\"/>\n",
"<!-- page_view -->\n",
"<g id=\"node1\" class=\"node\">\n",
"<title>page_view</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"48.19\" cy=\"-48.19\" rx=\"48.19\" ry=\"48.19\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"48.19\" y=\"-43.51\" font-family=\"Times,serif\" font-size=\"14.00\">page_view</text>\n",
"</g>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.graphs.Digraph at 0x7f6800f97110>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[0.]]\n",
"f0317a5d-e424-44e9-b784-c8f7291ffe31\n"
]
},
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 13.1.2 (0)\n",
" -->\n",
"<!-- Pages: 1 -->\n",
"<svg width=\"310pt\" height=\"160pt\"\n",
" viewBox=\"0.00 0.00 310.00 160.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 156.44)\">\n",
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-156.44 305.89,-156.44 305.89,4 -4,4\"/>\n",
"<!-- page_view -->\n",
"<g id=\"node1\" class=\"node\">\n",
"<title>page_view</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"48.19\" cy=\"-69.01\" rx=\"48.19\" ry=\"48.19\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"48.19\" y=\"-64.33\" font-family=\"Times,serif\" font-size=\"14.00\">page_view</text>\n",
"</g>\n",
"<!-- page_view&#45;&gt;page_view -->\n",
"<g id=\"edge1\" class=\"edge\">\n",
"<title>page_view&#45;&gt;page_view</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M33.03,-115.09C34.09,-126.6 39.14,-135.19 48.19,-135.19 53.98,-135.19 58.13,-131.66 60.65,-126.1\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"64.01,-127.11 62.98,-116.56 57.21,-125.45 64.01,-127.11\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"48.19\" y=\"-139.14\" font-family=\"Times,serif\" font-size=\"14.00\">0.50</text>\n",
"</g>\n",
"<!-- view_item_page -->\n",
"<g id=\"node2\" class=\"node\">\n",
"<title>view_item_page</title>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"232.88\" cy=\"-69.01\" rx=\"69.01\" ry=\"69.01\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"232.88\" y=\"-64.33\" font-family=\"Times,serif\" font-size=\"14.00\">view_item_page</text>\n",
"</g>\n",
"<!-- page_view&#45;&gt;view_item_page -->\n",
"<g id=\"edge2\" class=\"edge\">\n",
"<title>page_view&#45;&gt;view_item_page</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M96.71,-69.01C113.69,-69.01 133.31,-69.01 152.25,-69.01\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"152.1,-72.51 162.1,-69.01 152.1,-65.51 152.1,-72.51\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"130.12\" y=\"-72.96\" font-family=\"Times,serif\" font-size=\"14.00\">0.50</text>\n",
"</g>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.graphs.Digraph at 0x7f6800bf50f0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[5.0e-001 5.0e-001]\n",
" [9.9e-324 1.5e-323]]\n"
]
}
],
"source": [
"def explore_session(session_id: str):\n",
" subset = df[df['sessionId'] == session_id]\n",
" print(session_id)\n",
" P, labels = build_transition_prob_matrix(subset)\n",
" g = render_graph(f\"session_{session_id}\", P, ls_index=labels, threshold=0.01, fmt=\"svg\", view=False)\n",
" display(g)\n",
" return P\n",
"for session in sessions:\n",
" print(explore_session(session))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (PHANTOM)",
"language": "python",
"name": "phantom"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
import pytest
import pandas as pd
import numpy as np
from procesing.steps.session import (
TemporalFeatureStep,
BehavioralFeatureStep,
ProductFeatureStep,
UserAgentFeatureStep,
ExtractSessionFeaturesStep,
JoinLabelsStep,
ValidateDataStep,
)
# TemporalFeatureStep tests
def test_temporal_empty(pipeline_context):
result = TemporalFeatureStep(pipeline_context).transform(pd.DataFrame())
assert 'sessionId' in result.columns
assert result.empty
def test_temporal_basic(pipeline_context, session_interactions):
result = TemporalFeatureStep(pipeline_context).transform(session_interactions)
assert 'session_duration_sec' in result.columns
assert 'interaction_velocity' in result.columns
assert 'max_velocity_5min' in result.columns
assert result['total_interactions'].sum() == len(session_interactions)
def test_temporal_timeout(pipeline_context):
df = pd.DataFrame({
'sessionId': ['s1', 's1'],
'ts': ['2025-01-01T10:00:00Z', '2025-01-01T11:00:00Z'], # 1 hour gap
})
result = TemporalFeatureStep(pipeline_context, timeout_sec=900).transform(df)
assert result.iloc[0]['session_duration_sec'] == 0 # gap exceeds timeout
# BehavioralFeatureStep tests
def test_behavioral_empty(pipeline_context):
result = BehavioralFeatureStep(pipeline_context).transform(pd.DataFrame())
assert 'sessionId' in result.columns
def test_behavioral_counts(pipeline_context, session_interactions):
result = BehavioralFeatureStep(pipeline_context).transform(session_interactions)
assert 'page_views' in result.columns
assert 'item_views' in result.columns
assert 'hover_events' in result.columns
assert result['total_events'].sum() == len(session_interactions)
def test_behavioral_hover_prefix(pipeline_context):
df = pd.DataFrame({
'sessionId': ['s1', 's1'],
'eventName': ['hover_over_custom', 'hover_over_button'],
'page': ['/products', '/products'],
})
result = BehavioralFeatureStep(pipeline_context).transform(df)
assert result.iloc[0]['hover_events'] == 2
# ProductFeatureStep tests
def test_product_empty(pipeline_context):
result = ProductFeatureStep(pipeline_context).transform(pd.DataFrame())
assert 'sessionId' in result.columns
def test_product_features(pipeline_context, session_interactions):
result = ProductFeatureStep(pipeline_context).transform(session_interactions)
assert 'unique_products_viewed' in result.columns
assert 'price_range' in result.columns
assert result['unique_products_viewed'].sum() > 0
# UserAgentFeatureStep tests
def test_ua_empty(pipeline_context):
result = UserAgentFeatureStep(pipeline_context).transform(pd.DataFrame())
assert 'sessionId' in result.columns
def test_ua_headless_detection(pipeline_context):
df = pd.DataFrame({
'sessionId': ['s1', 's2'],
'userAgent': ['Mozilla/5.0 Chrome/120', 'HeadlessChrome/120'],
})
result = UserAgentFeatureStep(pipeline_context).transform(df)
assert 'is_headless' in result.columns
headless = dict(zip(result['sessionId'], result['is_headless']))
assert headless['s1'] == False
assert headless['s2'] == True
def test_ua_browser_family(pipeline_context):
df = pd.DataFrame({
'sessionId': ['s1', 's2', 's3'],
'userAgent': ['Mozilla/5.0 Firefox/120', 'Safari/605.1.15', 'Unknown'],
})
result = UserAgentFeatureStep(pipeline_context).transform(df)
browsers = dict(zip(result['sessionId'], result['browser_family']))
assert browsers['s1'] == 'Firefox'
assert browsers['s2'] == 'Safari'
assert browsers['s3'] == 'Other'
def test_ua_automation_detection(pipeline_context):
df = pd.DataFrame({
'sessionId': ['s1', 's2'],
'userAgent': ['Selenium WebDriver', 'Normal Chrome/120'],
})
result = UserAgentFeatureStep(pipeline_context).transform(df)
auto = dict(zip(result['sessionId'], result['is_automation']))
assert auto['s1'] == True
assert auto['s2'] == False
# ExtractSessionFeaturesStep tests
def test_extract_empty(pipeline_context):
result = ExtractSessionFeaturesStep(pipeline_context).transform(pd.DataFrame())
assert result.empty
def test_extract_merges_all(pipeline_context, session_interactions):
result = ExtractSessionFeaturesStep(pipeline_context).transform(session_interactions)
expected = ['session_duration_sec', 'total_events', 'unique_products_viewed', 'is_headless']
for col in expected:
assert col in result.columns
assert 'experimentId' in result.columns
# JoinLabelsStep tests
def test_join_labels_tuple_input(pipeline_context):
features = pd.DataFrame({'sessionId': ['s1'], 'experimentId': ['exp1'], 'total_events': [5]})
experiments = pd.DataFrame({'id': ['exp1'], 'xp_human_only': [True]})
result = JoinLabelsStep(pipeline_context).transform((features, experiments))
assert 'is_agent' in result.columns
assert result.iloc[0]['is_agent'] == False
def test_join_labels_empty_experiments(pipeline_context):
features = pd.DataFrame({'sessionId': ['s1'], 'experimentId': ['exp1']})
result = JoinLabelsStep(pipeline_context).transform((features, pd.DataFrame()))
assert pd.isna(result.iloc[0]['is_agent'])
# ValidateDataStep tests
def test_validate_empty(pipeline_context):
ValidateDataStep(pipeline_context).transform(pd.DataFrame())
report = pipeline_context.get_cached('validation_report')
assert report['status'] == 'empty'
def test_validate_missing_cols(pipeline_context):
df = pd.DataFrame({'sessionId': ['s1'], 'ts': ['2025-01-01']})
ValidateDataStep(pipeline_context).transform(df)
report = pipeline_context.get_cached('validation_report')
assert report['status'] == 'invalid'
assert 'eventName' in report['missing_cols']
def test_validate_valid(pipeline_context, session_interactions):
ValidateDataStep(pipeline_context).transform(session_interactions)
report = pipeline_context.get_cached('validation_report')
assert report['status'] == 'valid'
assert report['sessions'] > 0

View File

@@ -2,11 +2,14 @@ import redis
import pickle
import json
import pandas as pd
from io import StringIO
from typing import Optional, Dict, Any
import os
import logging
log = logging.getLogger(__name__)
class ModelRegistry:
"""
Lightweight model registry using Redis for storing pricing models and elasticity data.
@@ -14,24 +17,23 @@ class ModelRegistry:
"""
def __init__(self, redis_host: str = None, redis_port: int = None):
host = redis_host or os.getenv('REDIS_HOST', 'localhost')
port = redis_port or int(os.getenv('REDIS_PORT', '6378'))
host = redis_host or os.getenv("REDIS_HOST", "localhost")
port = redis_port or int(os.getenv("REDIS_PORT", "6378"))
self.redis_client = redis.Redis(
host=host,
port=port,
db=0,
decode_responses=False
host=host, port=port, db=0, decode_responses=False
)
self.metadata_prefix = "model:meta:"
self.data_prefix = "model:data:"
self.elasticity_prefix = "elasticity:"
self.prices_prefix = "prices:"
def publish_elasticity(self,
elasticity_df: pd.DataFrame,
model_name: str = 'latest',
metadata: Optional[Dict[str, Any]] = None):
def publish_elasticity(
self,
elasticity_df: pd.DataFrame,
model_name: str = "latest",
metadata: Optional[Dict[str, Any]] = None,
):
"""
Store elasticity estimates in registry.
@@ -43,25 +45,29 @@ class ModelRegistry:
key = f"{self.elasticity_prefix}{model_name}"
# serialize dataframe as JSON
data_json = elasticity_df.to_json(orient='records')
data_json = elasticity_df.to_json(orient="records")
# store data
self.redis_client.set(key, data_json)
# store metadata
meta = metadata or {}
meta.update({
'n_products': len(elasticity_df),
'mean_elasticity': float(elasticity_df['elasticity'].mean()),
'model_type': 'elasticity_snapshot'
})
meta.update(
{
"n_products": len(elasticity_df),
"mean_elasticity": float(elasticity_df["elasticity"].mean()),
"model_type": "elasticity_snapshot",
}
)
meta_key = f"{self.metadata_prefix}{model_name}"
self.redis_client.set(meta_key, json.dumps(meta))
log.info(f"Published elasticity model '{model_name}' with {len(elasticity_df)} products")
log.info(
f"Published elasticity model '{model_name}' with {len(elasticity_df)} products"
)
def get_elasticity(self, model_name: str = 'latest') -> Optional[pd.DataFrame]:
def get_elasticity(self, model_name: str = "latest") -> Optional[pd.DataFrame]:
"""Retrieve elasticity estimates from registry."""
key = f"{self.elasticity_prefix}{model_name}"
data_json = self.redis_client.get(key)
@@ -71,14 +77,16 @@ class ModelRegistry:
# decode bytes to string if needed
if isinstance(data_json, bytes):
data_json = data_json.decode('utf-8')
data_json = data_json.decode("utf-8")
return pd.read_json(data_json, orient='records')
return pd.read_json(StringIO(data_json), orient="records")
def publish_pricing_model(self,
pricing_function,
model_name: str = 'latest',
metadata: Optional[Dict[str, Any]] = None):
def publish_pricing_model(
self,
pricing_function,
model_name: str = "latest",
metadata: Optional[Dict[str, Any]] = None,
):
"""
Store a fitted pricing function object.
@@ -95,17 +103,19 @@ class ModelRegistry:
# store metadata
meta = metadata or {}
meta.update({
'model_class': pricing_function.__class__.__name__,
'model_type': 'pricing_function'
})
meta.update(
{
"model_class": pricing_function.__class__.__name__,
"model_type": "pricing_function",
}
)
meta_key = f"{self.metadata_prefix}{model_name}"
self.redis_client.set(meta_key, json.dumps(meta))
log.info(f"Published pricing model '{model_name}' ({meta['model_class']})")
def get_pricing_model(self, model_name: str = 'latest'):
def get_pricing_model(self, model_name: str = "latest"):
"""Retrieve a pricing function from registry."""
key = f"{self.data_prefix}{model_name}"
model_bytes = self.redis_client.get(key)
@@ -120,21 +130,23 @@ class ModelRegistry:
models = {}
for key in self.redis_client.scan_iter(f"{self.metadata_prefix}*"):
key_str = key.decode('utf-8') if isinstance(key, bytes) else key
model_name = key_str.replace(self.metadata_prefix, '')
key_str = key.decode("utf-8") if isinstance(key, bytes) else key
model_name = key_str.replace(self.metadata_prefix, "")
meta_json = self.redis_client.get(key)
if meta_json:
if isinstance(meta_json, bytes):
meta_json = meta_json.decode('utf-8')
meta_json = meta_json.decode("utf-8")
models[model_name] = json.loads(meta_json)
return models
def publish_prices(self,
prices_df: pd.DataFrame,
model_name: str = 'latest',
metadata: Optional[Dict[str, Any]] = None):
def publish_prices(
self,
prices_df: pd.DataFrame,
model_name: str = "latest",
metadata: Optional[Dict[str, Any]] = None,
):
"""Store predicted prices in registry.
Args:
@@ -143,22 +155,19 @@ class ModelRegistry:
metadata: additional info
"""
key = f"{self.prices_prefix}{model_name}"
data_json = prices_df.to_json(orient='records')
data_json = prices_df.to_json(orient="records")
self.redis_client.set(key, data_json)
meta = metadata or {}
meta.update({
'n_products': len(prices_df),
'model_type': 'predicted_prices'
})
meta.update({"n_products": len(prices_df), "model_type": "predicted_prices"})
meta_key = f"{self.metadata_prefix}prices_{model_name}"
self.redis_client.set(meta_key, json.dumps(meta))
log.info(f"Published prices '{model_name}' for {len(prices_df)} products")
def get_prices(self, model_name: str = 'latest') -> Optional[pd.DataFrame]:
def get_prices(self, model_name: str = "latest") -> Optional[pd.DataFrame]:
"""Retrieve predicted prices from registry."""
key = f"{self.prices_prefix}{model_name}"
data_json = self.redis_client.get(key)
@@ -167,9 +176,9 @@ class ModelRegistry:
return None
if isinstance(data_json, bytes):
data_json = data_json.decode('utf-8')
data_json = data_json.decode("utf-8")
return pd.read_json(data_json, orient='records')
return pd.read_json(StringIO(data_json), orient="records")
def health_check(self) -> bool:
"""Check if Redis connection is alive."""
@@ -179,7 +188,9 @@ class ModelRegistry:
except:
return False
def set_session_prices(self, session_id: str, prices: Dict[str, float], ttl: int = 1800):
def set_session_prices(
self, session_id: str, prices: Dict[str, float], ttl: int = 1800
):
"""
Store prices for a specific session.
THIS is the write path for session-aware pricing.
@@ -210,7 +221,9 @@ class ModelRegistry:
if price_str is None:
return None
return float(price_str.decode('utf-8') if isinstance(price_str, bytes) else price_str)
return float(
price_str.decode("utf-8") if isinstance(price_str, bytes) else price_str
)
def get_session_all_prices(self, session_id: str) -> Dict[str, float]:
"""Get all prices for a session."""
@@ -221,6 +234,8 @@ class ModelRegistry:
return {}
return {
(k.decode('utf-8') if isinstance(k, bytes) else k): float(v.decode('utf-8') if isinstance(v, bytes) else v)
(k.decode("utf-8") if isinstance(k, bytes) else k): float(
v.decode("utf-8") if isinstance(v, bytes) else v
)
for k, v in prices_raw.items()
}

128
lib/separability.py Normal file
View File

@@ -0,0 +1,128 @@
"""Utilities for loading separability artifacts and scoring interaction sessions."""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Sequence
import joblib
import numpy as np
from experiments.ml.arch import featurize_trajectory
DEFAULT_ARTIFACT_DIR = Path("data/separability")
@dataclass
class SeparabilityArtifacts:
scaler: object
classifier: object
states: List[str]
event_transitions: Dict[str, Dict[str, float]]
feature_dim: int
def _normalize_events(raw_events: Sequence[object]) -> List[object]:
events: List[object] = []
for evt in raw_events:
if hasattr(evt, "value") and hasattr(evt.value, "payload"):
events.append(evt.value.payload)
else:
events.append(evt)
events.sort(key=lambda e: getattr(e, "ts", ""))
return events
def _event_transition_distribution(events: Sequence[object]) -> Dict[str, Dict[str, float]]:
counts: Dict[str, Dict[str, int]] = {}
for src_evt, dst_evt in zip(events, events[1:]):
src_name = getattr(src_evt, "eventName", "unknown")
dst_name = getattr(dst_evt, "eventName", "unknown")
counts.setdefault(src_name, {})
counts[src_name][dst_name] = counts[src_name].get(dst_name, 0) + 1
distribution: Dict[str, Dict[str, float]] = {}
for src, dsts in counts.items():
total = float(sum(dsts.values()))
distribution[src] = {dst: val / total for dst, val in dsts.items()} if total else {}
return distribution
def _kl_divergence(p: Dict[str, Dict[str, float]], q: Dict[str, Dict[str, float]]) -> float:
eps = 1e-10
total = 0.0
for src, dsts in p.items():
for dst, prob in dsts.items():
ref = q.get(src, {}).get(dst, 0.0)
total += (prob + eps) * np.log((prob + eps) / (ref + eps))
return float(total)
def load_artifacts(artifact_dir: Path | str = DEFAULT_ARTIFACT_DIR) -> SeparabilityArtifacts:
artifact_dir = Path(artifact_dir)
scaler_path = artifact_dir / "scaler.joblib"
model_path = artifact_dir / "classifier.joblib"
metadata_path = artifact_dir / "metadata.json"
if not (scaler_path.exists() and model_path.exists() and metadata_path.exists()):
raise FileNotFoundError(
f"Separability artifacts not found in {artifact_dir}. Run sim.strong_learner.train first."
)
scaler = joblib.load(scaler_path)
classifier = joblib.load(model_path)
with open(metadata_path, "r", encoding="utf-8") as fin:
metadata = json.load(fin)
return SeparabilityArtifacts(
scaler=scaler,
classifier=classifier,
states=list(metadata["reference_states"]),
event_transitions=metadata["event_transitions"],
feature_dim=int(metadata["feature_dim"]),
)
def score_session(
raw_events: Sequence[object],
artifacts: SeparabilityArtifacts,
) -> dict:
events = _normalize_events(raw_events)
if not events:
return {"prob_agent": 0.0, "delta_h": 0.0, "delta_a": 0.0}
reference_mdp = {"states": artifacts.states}
features = featurize_trajectory(events, mdp=reference_mdp, input_dim=artifacts.feature_dim)
scaled = artifacts.scaler.transform(features.reshape(1, -1))
prob_agent = float(artifacts.classifier.predict_proba(scaled)[0, 1])
session_dist = _event_transition_distribution(events)
delta_h = _kl_divergence(session_dist, artifacts.event_transitions.get("human", {}))
delta_a = _kl_divergence(session_dist, artifacts.event_transitions.get("agent", {}))
return {
"prob_agent": prob_agent,
"delta_h": delta_h,
"delta_a": delta_a,
}
def estimate_alpha(prob_agent: float, delta_h: float, delta_a: float, temperature: float = 1.0) -> float:
divergence_mass = delta_h + delta_a
if divergence_mass <= 1e-8:
return float(prob_agent)
ratio = delta_a / divergence_mass
blended = 0.5 * prob_agent + 0.5 * ratio
if temperature <= 0:
return float(np.clip(blended, 0.0, 1.0))
scaled = 1.0 / (1.0 + np.exp(-temperature * (blended - 0.5)))
return float(np.clip(scaled, 0.0, 1.0))
def score_sessions(raw_sessions: Iterable[Sequence[object]], artifacts: SeparabilityArtifacts) -> List[dict]:
return [score_session(events, artifacts) for events in raw_sessions]

71
nx.json Normal file
View File

@@ -0,0 +1,71 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"useInferencePlugins": false,
"defaultBase": "main",
"namedInputs": {
"sharedGlobals": [
"{workspaceRoot}/nx.json",
"{workspaceRoot}/package.json",
"{workspaceRoot}/Makefile",
"{workspaceRoot}/pyproject.toml",
"{workspaceRoot}/docker-compose.yml"
],
"default": [
"{projectRoot}/**/*",
"sharedGlobals"
],
"production": [
"default",
"!{projectRoot}/node_modules/**/*",
"!{projectRoot}/.next/**/*",
"!{projectRoot}/test-results/**/*",
"!{projectRoot}/build/**/*"
]
},
"targetDefaults": {
"build": {
"cache": true,
"inputs": [
"production",
"^production"
]
},
"test": {
"cache": false,
"inputs": [
"default",
"^production"
]
},
"install": {
"cache": false
},
"dev": {
"cache": false
},
"start": {
"cache": false
},
"watch": {
"cache": false
},
"clean": {
"cache": false
},
"train": {
"cache": false
},
"benchmark": {
"cache": false
},
"up": {
"cache": false
},
"down": {
"cache": false
},
"logs": {
"cache": false
}
}
}

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "phantom-monorepo",
"private": true,
"workspaces": [
"web",
"tests/e2e"
],
"scripts": {
"nx": "nx",
"projects": "nx show projects",
"graph": "nx graph",
"web:dev": "nx run web:dev",
"web:build": "nx run web:build",
"backend:server": "nx run backend-server:dev",
"backend:provider": "nx run pricing-provider:dev",
"backend:worker": "nx run backend-worker:dev",
"paper:build": "nx run paper:build",
"platform:up": "nx run platform:up",
"platform:down": "nx run platform:down",
"platform:logs": "nx run platform:logs",
"research:test": "nx run research:test",
"research:benchmark": "nx run research:benchmark",
"research:benchmark:simple": "nx run research:benchmark-simple",
"e2e:test": "nx run e2e:test"
},
"devDependencies": {
"nx": "^20.4.0"
}
}

View File

@@ -42,6 +42,10 @@ EOF
# Process each directory
echo "Concatenating code from source directories..."
# Engine
find "$PROJECT_ROOT/engine" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
add_file "$file"
done
# Backend
find "$PROJECT_ROOT/backend" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
add_file "$file"
@@ -53,7 +57,7 @@ find "$PROJECT_ROOT/experiments" -type d \( -name ".venv" -o -name "__pycache__"
done
# Docker
find "$PROJECT_ROOT/docker" -type d \( -name ".venv" -o -name "__pycache__" -o -name "node_modules" \) -prune -o -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" -o -name "Dockerfile*" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
find "$PROJECT_ROOT/docker" -type d \( -name ".venv" -o -name "__pycache__" -o -name "node_modules" \) -prune -o -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" -o -name "*.Dockerfile*" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
add_file "$file"
done

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
from scenes import SCENE_ORDER
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Render thesis-defense Manim scenes")
parser.add_argument(
"--quality",
default="qm",
choices=["ql", "qm", "qh", "qk"],
help="Manim quality preset",
)
parser.add_argument(
"--scene",
action="append",
dest="scenes",
help="Scene name; repeat flag to render many",
)
parser.add_argument(
"--preview", action="store_true", help="Open video after each render"
)
parser.add_argument(
"--list", action="store_true", help="List available scenes 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_ORDER]
if missing:
choices = ", ".join(SCENE_ORDER)
raise ValueError(f"Unknown scenes: {', '.join(missing)}. Choices: {choices}")
return requested
def run_manim(scene_file: Path, scene_name: str, quality: str, preview: bool) -> None:
cmd = [sys.executable, "-m", "manim"]
if preview:
cmd.append("-p")
cmd.extend([f"-{quality}", str(scene_file), scene_name])
subprocess.run(cmd, cwd=scene_file.parent, check=True)
def main() -> int:
args = parse_args()
if args.list:
for scene in SCENE_ORDER:
print(scene)
return 0
scenes = validate_requested(args.scenes) if args.scenes else list(SCENE_ORDER)
scene_file = Path(__file__).resolve().parent / "scenes.py"
try:
for scene_name in scenes:
run_manim(
scene_file=scene_file,
scene_name=scene_name,
quality=args.quality,
preview=args.preview,
)
except FileNotFoundError:
print(
"manim executable not found. Install Manim in your Python environment.",
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())

View File

@@ -0,0 +1,2 @@
manim>=0.18,<1
numpy>=1.24

File diff suppressed because it is too large Load Diff

53
paper/project.json Normal file
View File

@@ -0,0 +1,53 @@
{
"$schema": "../node_modules/nx/schemas/project-schema.json",
"name": "paper",
"projectType": "application",
"sourceRoot": "paper/src",
"targets": {
"build": {
"executor": "nx:run-commands",
"outputs": [
"{projectRoot}/build"
],
"options": {
"command": "bash scripts/nx_paper.sh build",
"cwd": "."
}
},
"watch": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_paper.sh watch",
"cwd": "."
}
},
"clean": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_paper.sh clean",
"cwd": "."
}
},
"wordcount": {
"executor": "nx:run-commands",
"options": {
"command": "bash scripts/nx_paper.sh wordcount",
"cwd": "."
}
},
"build-arxiv": {
"executor": "nx:run-commands",
"outputs": [
"{projectRoot}/build/main-arxiv.pdf"
],
"options": {
"command": "bash scripts/nx_paper.sh build-arxiv",
"cwd": "."
}
}
},
"tags": [
"scope:paper",
"type:latex"
]
}

View File

@@ -16,7 +16,6 @@
"chapters/04-results"
"chapters/05-discussion"
"chapters/06-conclusion"
"../build/concatenated_code"
"article"
"art12"))
:latex)

View File

@@ -26,7 +26,7 @@
file = {PDF:/home/velocitatem/Zotero/storage/Q7J5EBEJ/3447815.pdf:application/pdf},
}
@phdthesis{salassa_politecnico_nodate,
@phdthesis{salassa_politecnico_2024,
title = {Politecnico di {Torino} {Algorithmic} {Pricing} in the digital age "{Ethical} considerations on its economic and social implications, and an analysis of possible solutions to overcome its critical issues" {Tutor}: {Candidate}},
abstract = {Algorithmic pricing is an emerging business practice that uses computational algorithms to determine
the prices of products and services based on a number of dynamic factors. The aim of this thesis is to
@@ -50,6 +50,8 @@ laws, for fair and non-discriminatory use.},
urldate = {2025-11-12},
school = {Politecnico di Torino},
author = {Salassa, Fabio and Pautassi, Paolo},
month = apr,
year = {2024},
file = {PDF:/home/velocitatem/Zotero/storage/L95WYQ8B/m-api-06aad998-d926-0d59-5593-82fdce5a678b.pdf:application/pdf},
}
@@ -62,11 +64,12 @@ laws, for fair and non-discriminatory use.},
file = {PDF:/home/velocitatem/Zotero/storage/IZD3C5SR/m-api-26f6207c-cc89-4aed-29b6-34629f18fe9b.pdf:application/pdf},
}
@article{shahidi_coasean_nodate,
@article{shahidi_coasean_2025,
title = {The {Coasean} {Singularity}? {Demand}, {Supply}, and {Market} {Design} with {AI} {Agents}},
abstract = {AI agents—autonomous systems that perceive, reason, and act on behalf of human principals—are poised to transform digital markets by dramatically reducing transaction costs. This chapter evaluates the economic implications of this transition, adopting a consumeroriented view of agents as market participants that can search, negotiate, and transact directly. From the demand side, agent adoption reflects derived demand: users trade off decision quality against effort reduction, with outcomes mediated by agent capability and task context. On the supply side, firms will design, integrate, and monetize agents, with outcomes hinging on whether agents operate within or across platforms. At the market level, agents create efficiency gains from lower search, communication, and contracting costs, but also introduce frictions such as congestion and price obfuscation. By lowering the costs of preference elicitation, contract enforcement, and identity verification, agents expand the feasible set of market designs but also raise novel regulatory challenges. While the net welfare effects remain an empirical question, the rapid onset of AI-mediated transactions presents a unique opportunity for economic research to inform real-world policy and market design.},
language = {en},
author = {Shahidi, Peyman and Rusak, Gili and Manning, Benjamin S and Fradkin, Andrey and Horton, John J},
year = {2025},
file = {PDF:/home/velocitatem/Zotero/storage/TQCAPJDP/Shahidi et al. - The Coasean Singularity Demand, Supply, and Market Design with AI Agents.pdf:application/pdf},
}
@@ -84,10 +87,14 @@ laws, for fair and non-discriminatory use.},
file = {PDF:/home/velocitatem/Zotero/storage/ZLJQ4DQ9/Byrnes - 2025 - Intro to Brain-Like-AGI Safety.pdf:application/pdf},
}
@article{shannon_mathematical_nodate,
@article{shannon_mathematical_1948,
title = {A {Mathematical} {Theory} of {Communication}},
volume = {27},
language = {en},
journal = {Bell System Technical Journal},
author = {Shannon, C E},
month = oct,
year = {1948},
file = {PDF:/home/velocitatem/Zotero/storage/FJRFRWK2/Shannon - A Mathematical Theory of Communication.pdf:application/pdf},
}
@@ -96,11 +103,13 @@ laws, for fair and non-discriminatory use.},
file = {PDF:/home/velocitatem/Zotero/storage/D3QRGY9Z/order_stats.pdf:application/pdf},
}
@article{devine_nonlinear_nodate,
@article{devine_nonlinear_2017,
title = {Nonlinear {Pricing} with {Costly} {Information} {Acquisition}},
abstract = {This paper examines a nonlinear pricing model where the firm can choose to acquire costly information prior to offering contract menus to consumers; such as paying a consultant or investing in machine learning technologies. Information provides the firm with a signal about consumers types, whose accuracy increases as the firm acquires larger amounts of information. We show that the firm chooses to acquire information, only if it can purchase a sufficient amount that could alter its initial prior beliefs. Relative to standard settings where firms cannot acquire information, we identify how information acquisition changes optimal contract offers, equilibrium profits, information rents, and welfare. A better-informed firm increases its expected profits, but it can also increase expected utility when the cost of information is intermediate. Our results recommend balanced online privacy laws.},
language = {en},
author = {Devine, Brett R and Munoz-Garcia, Felix},
month = nov,
year = {2017},
file = {PDF:/home/velocitatem/Zotero/storage/GQ28KVBF/Devine and Munoz-Garcia - Nonlinear Pricing with Costly Information Acquisition.pdf:application/pdf},
}
@@ -202,10 +211,11 @@ laws, for fair and non-discriminatory use.},
file = {PDF:/home/velocitatem/Zotero/storage/U7A5Q78V/Karten et al. - 2025 - LLM Economist Large Population Models and Mechanism Design in Multi-Agent Generative Simulacra.pdf:application/pdf},
}
@techreport{mullapudi_reinforcement_nodate,
@techreport{mullapudi_reinforcement_2025,
title = {A {Reinforcement} {Learning} {Approach} to {Dynamic} {Pricing}},
abstract = {Dynamic pricing represents a critical strategic challenge in modern e-commerce, where firms must navigate fluctuating demand, inventory constraints, and aggressive competitor actions. Traditional static and heuristic-based pricing models often fail to capture the complex, non-linear dynamics of competitive digital markets, leading to suboptimal profitability. This paper proposes a model-free reinforcement learning (RL) framework to address this challenge. Specifically, we design, implement, and evaluate a Q-learning agent capable of learning an optimal, state-dependent pricing policy. The agent is trained and evaluated within a simulated market environment constructed from the publicly available "Retail Price Optimization" dataset from Kaggle, which provides a rich feature set including historical sales, product characteristics, seasonality, and, crucially, competitor pricing data. The problem is formulated as a Markov Decision Process (MDP), where the agent's state incorporates its price position relative to competitors, competitor price trends, and seasonal factors. The agent's performance is benchmarked against three baseline strategies: static pricing, a reactive "follow-the-leader" heuristic, and random pricing. The results demonstrate that the Q-learning agent achieves a substantial increase in total cumulative profit over the evaluation period, outperforming all baselines by learning a nuanced policy that strategically balances price adjustments in response to market conditions. This work provides a practical and reproducible blueprint for applying reinforcement learning to optimize pricing decisions in a simulated yet realistic competitive retail environment, highlighting the potential of RL to automate complex strategic decision-making.},
author = {Mullapudi, Pavan},
year = {2025},
note = {Publication Title: International Journal on Science and Technology (IJSAT) IJSAT25049558
Volume: 16
Issue: 4},
@@ -294,10 +304,11 @@ Issue: 4},
file = {PDF:/home/velocitatem/Zotero/storage/S8635QX6/varian95a.pdf:application/pdf},
}
@book{russell_artificial_nodate,
@book{russell_artificial_2021,
title = {Artificial {Intelligence} {A} {Modern} {Approach} {Fourth} {Edition} {Global} {Edition}},
isbn = {978-1-292-40117-1},
author = {Russell, Stuart and Norvig, Peter},
year = {2021},
file = {PDF:/home/velocitatem/Zotero/storage/6B8W8S27/efdd4d1d4c2087fe1cbe03d9ced67f34.pdf:application/pdf},
}
@@ -312,10 +323,11 @@ Volume: 21},
file = {PDF:/home/velocitatem/Zotero/storage/N9JNXFJW/live-1333-2265-jair.pdf:application/pdf},
}
@techreport{shoham_multiagent_nodate,
@techreport{shoham_multiagent_2009,
title = {Multiagent {Systems}: {Algorithmic}, {Game}-{Theoretic}, and {Logical} {Foundations}},
url = {http://www.masfoundations.org.},
author = {Shoham, Yoav and Leyton-Brown, Kevin},
year = {2009},
keywords = {algorithms, auctions, communication, competition, cooperation, distributed problem solving, game theory, learning, logic, mechanism design, social choice},
file = {PDF:/home/velocitatem/Zotero/storage/QZVYS7V9/shoham09a.pdf:application/pdf},
}
@@ -331,11 +343,13 @@ Volume: 21},
file = {PDF:/home/velocitatem/Zotero/storage/H8IS64AW/2411.13768v2.pdf:application/pdf},
}
@techreport{xie_osworld_nodate,
@techreport{xie_osworld_2024,
title = {{OSWORLD}: {Benchmarking} {Multimodal} {Agents} for {Open}-{Ended} {Tasks} in {Real} {Computer} {Environments}},
url = {https://os-world.github.io},
abstract = {Autonomous agents that accomplish complex computer tasks with minimal human interventions have the potential to transform human-computer interaction, significantly enhancing accessibility and productivity. However, existing benchmarks either lack an interactive environment or are limited to environments specific to certain applications or domains, failing to reflect the diverse and complex nature of real-world computer use, thereby limiting the scope of tasks and agent scalability. To address this issue, we introduce OSWORLD, the first-of-its-kind scalable, real computer environment for multimodal agents, supporting task setup, execution-based evaluation, and interactive learning across various operating systems such as Ubuntu, Windows, and macOS. OSWORLD can serve as a unified, integrated computer environment for assessing open-ended computer tasks that involve arbitrary applications. Building upon OSWORLD, we create a benchmark of 369 computer tasks involving real web and desktop apps in open domains, OS file I/O, and workflows spanning multiple applications. Each task example is derived from real-world computer use cases and includes a detailed initial state setup configuration and a custom execution-based evaluation script for reliable, reproducible evaluation. Extensive evaluation of state-of-the-art LLM/VLM-based agents on OSWORLD reveals significant deficiencies in their ability to serve as computer assistants. While humans can accomplish over 72.36\% of the tasks, the best model achieves only 12.24\% success, primarily struggling with GUI grounding and operational knowledge. Comprehensive analysis using OSWORLD provides valuable insights for developing multimodal generalist agents that were not possible with previous benchmarks. Our code, environment, baseline models, and data are publicly available at https://os-world.github.io.},
author = {Xie, Tianbao and Zhang, Danyang and Chen, Jixuan and Li, Xiaochuan and Zhao, Siheng and Cao, Ruisheng and Jing Hua, Toh and Cheng, Zhoujun and Shin, Dongchan and Lei, Fangyu and Liu, Yitao and Xu, Yiheng and Zhou, Shuyan and Savarese, Silvio and Xiong, Caiming and Zhong, Victor and Yu, Tao},
month = may,
year = {2024},
note = {arXiv: 2404.07972v2},
file = {PDF:/home/velocitatem/Zotero/storage/LLRKXIC7/full-text.pdf:application/pdf},
}
@@ -364,17 +378,21 @@ Volume: 21},
file = {PDF:/home/velocitatem/Zotero/storage/QNXZJLRM/S2444883425000038.pdf:application/pdf},
}
@misc{ghaffary_amazon_nodate,
@misc{ghaffary_amazon_2025,
title = {Amazon {Sues} to {Stop} {Perplexity} {From} {Using} {AI} {Tool} to {Buy} {Stuff}},
url = {https://www.bloomberg.com/news/articles/2025-11-04/amazon-demands-perplexity-stop-ai-agent-from-making-purchases},
author = {Ghaffary, Shirin and Day, Matt},
month = nov,
year = {2025},
file = {PDF:/home/velocitatem/Zotero/storage/IQL6FPWE/Amazon Sues to Stop Perplexity From Using AI Tool to Buy Stuff - Bloomberg.pdf:application/pdf},
}
@techreport{besbes_dynamic_nodate,
@techreport{besbes_dynamic_2007,
title = {Dynamic {Pricing} {Without} {Knowing} the {Demand} {Function}: {Risk} {Bounds} and {Near}-{Optimal} {Algorithms} *},
abstract = {We consider a single product revenue management problem where, given an initial inventory, the objective is to dynamically adjust prices over a finite sales horizon to maximize expected revenues. Realized demand is observed over time, but the underlying functional relationship between price and mean demand rate that governs these observations (otherwise known as the demand function or demand curve), is not known. We consider two instances of this problem: i.) a setting where the demand function is assumed to belong to a known parametric family with unknown parameter values; and ii.) a setting where the demand function is assumed to belong to a broad class of functions that need not admit any parametric representation. In each case we develop policies that learn the demand function "on the fly," and optimize prices based on that. The performance of these algorithms is measured in terms of the regret: the revenue loss relative to the maximal revenues that can be extracted when the demand function is known prior to the start of the selling season. We derive lower bounds on the regret that hold for any admissible pricing policy, and then show that our proposed algorithms achieve a regret that is "close" to this lower bound. The magnitude of the regret can be interpreted as the economic value of prior knowledge on the demand function; manifested as the revenue loss due to model uncertainty.},
author = {Besbes, Omar and Zeevi, Assaf},
month = dec,
year = {2007},
note = {Publication Title: Operations Research},
keywords = {learning, asymptotic analysis, estimation, exploration-exploitation, pricing, Revenue management, value of information},
file = {PDF:/home/velocitatem/Zotero/storage/SBAIB4V2/Dp_wo_demand_risk_ob_az_posted.pdf:application/pdf},
@@ -423,3 +441,192 @@ Volume: 21},
keywords = {Computer Science - Computation and Language},
file = {PDF:/home/velocitatem/Zotero/storage/3Z2XK4QC/Ganie - 2025 - Uncertainty in Authorship Why Perfect AI Detection Is Mathematically Impossible.pdf:application/pdf},
}
@article{shi_distributionally_2024,
title = {Distributionally {Robust} {Model}-{Based} {Offline} {Reinforcement} {Learning} with {Near}-{Optimal} {Sample} {Complexity}},
abstract = {This paper concerns the central issues of model robustness and sample efficiency in offline reinforcement learning (RL), which aims to learn to perform decision making from history data without active exploration. Due to uncertainties and variabilities of the environment, it is critical to learn a robust policy—with as few samples as possible—that performs well even when the deployed environment deviates from the nominal one used to collect the history dataset. We consider a distributionally robust formulation of offline RL, focusing on tabular robust Markov decision processes with an uncertainty set specified by the Kullback-Leibler divergence in both finite-horizon and infinite-horizon settings. To combat with sample scarcity, a model-based algorithm that combines distributionally robust value iteration with the principle of pessimism in the face of uncertainty is proposed, by penalizing the robust value estimates with a carefully designed data-driven penalty term. Under a mild and tailored assumption of the history dataset that measures distribution shift without requiring full coverage of the state-action space, we establish the finite-sample complexity of the proposed algorithms. We further develop an informationtheoretic lower bound, which suggests that learning RMDPs is at least as hard as the standard MDPs when the uncertainty level is sufficient small, and corroborates the tightness of our upper bound up to polynomial factors of the (effective) horizon length for a range of uncertainty levels. To the best our knowledge, this provides the first provably near-optimal robust offline RL algorithm that learns under model uncertainty and partial coverage.},
language = {en},
author = {Shi, Laixi and Chi, Yuejie},
month = jun,
year = {2024},
file = {PDF:/home/velocitatem/Zotero/storage/K56G4EIP/Shi and Chi - Distributionally Robust Model-Based Offline Reinforcement Learning with Near-Optimal Sample Complexity.pdf:application/pdf},
}
@article{dutting_mechanism_2025,
title = {Mechanism {Design} for {Large} {Language} {Models} ({Extended} {Abstract})},
abstract = {We investigate auction mechanisms for AIgenerated content, focusing on applications like ad creative generation. In our model, agents preferences over stochastically generated content are encoded as large language models (LLMs). We propose an auction format that operates on a tokenby-token basis, and allows LLM agents to influence content creation through single dimensional bids. We formulate two desirable incentive properties and prove their equivalence to a monotonicity condition on output aggregation. This equivalence enables a second-price rule design, even absent explicit agent valuation functions. Our design is supported by demonstrations on a publicly available LLM.},
language = {en},
author = {Dütting, Paul and Mirrokni, Vahab and Leme, Renato Paes and Xu, Haifeng and Zuo, Song},
year = {2025},
file = {PDF:/home/velocitatem/Zotero/storage/2ABDEYDN/Dütting et al. - Mechanism Design for Large Language Models (Extended Abstract).pdf:application/pdf},
}
@misc{fcmi_machine_2025,
title = {Machine {Speed} {Markets}: {AI} {Agent} {Market} {Strategy} \& {Growth}},
shorttitle = {Machine {Speed} {Markets}},
url = {https://www.360strategy.co.uk/post/machine-speed-markets-ai-agents},
abstract = {Recent research by NBER economists suggests these AI agents in particular, could drive a "Coasean singularity," a point where transaction costs fall towards zero, radically reshaping how markets function. In essence, tasks like finding information, negotiating deals, and enforcing contracts which are traditionally costly frictions in commerce, may become nearly instantaneous and costless.},
language = {en},
urldate = {2026-01-20},
journal = {360 Strategy},
author = {FCMi, CMgr, Mark Evans MBA},
month = nov,
year = {2025},
file = {Snapshot:/home/velocitatem/Zotero/storage/Z22P9JJH/machine-speed-markets-ai-agents.html:text/html},
}
@article{coase_nature_1937,
title = {The {Nature} of the {Firm}},
volume = {4},
issn = {1468-0335},
url = {https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1468-0335.1937.tb00002.x},
doi = {10.1111/j.1468-0335.1937.tb00002.x},
language = {en},
number = {16},
urldate = {2026-01-20},
journal = {Economica},
author = {Coase, R. H.},
year = {1937},
pages = {386--405},
file = {Full Text PDF:/home/velocitatem/Zotero/storage/TABLLPEU/Coase - 1937 - The Nature of the Firm.pdf:application/pdf;Snapshot:/home/velocitatem/Zotero/storage/Q5RFW9LJ/j.1468-0335.1937.tb00002.html:text/html},
}
@misc{fish_algorithmic_2025,
title = {Algorithmic {Collusion} by {Large} {Language} {Models}},
url = {http://arxiv.org/abs/2404.00806},
doi = {10.48550/arXiv.2404.00806},
abstract = {The rise of algorithmic pricing raises concerns of algorithmic collusion. We conduct experiments with algorithmic pricing agents based on Large Language Models (LLMs). We find that LLM-based pricing agents quickly and autonomously reach supracompetitive prices and profits in oligopoly settings and that variation in seemingly innocuous phrases in LLM instructions (“prompts”) may substantially influence the degree of supracompetitive pricing. Off-path analysis using novel techniques uncovers price-war concerns as contributing to these phenomena. Our results extend to auction settings. Our findings uncover unique challenges to any future regulation of LLM-based pricing agents, and AI-based pricing agents more broadly.},
language = {en},
urldate = {2026-01-20},
publisher = {arXiv},
author = {Fish, Sara and Gonczarowski, Yannai A. and Shorrer, Ran I.},
month = sep,
year = {2025},
note = {arXiv:2404.00806 [econ]},
keywords = {Computer Science - Computer Science and Game Theory, Computer Science - Artificial Intelligence, Economics - General Economics},
file = {PDF:/home/velocitatem/Zotero/storage/QHWVISCZ/Fish et al. - 2025 - Algorithmic Collusion by Large Language Models.pdf:application/pdf},
}
@misc{hardt_strategic_2015,
title = {Strategic {Classification}},
url = {http://arxiv.org/abs/1506.06980},
doi = {10.48550/arXiv.1506.06980},
abstract = {Machine learning relies on the assumption that unseen test instances of a classification problem follow the same distribution as observed training data. However, this principle can break down when machine learning is used to make important decisions about the welfare (employment, education, health) of strategic individuals. Knowing information about the classifier, such individuals may manipulate their attributes in order to obtain a better classification outcome. As a result of this behavior—often referred to as gaming—the performance of the classifier may deteriorate sharply. Indeed, gaming is a well-known obstacle for using machine learning methods in practice; in financial policy-making, the problem is widely known as Goodharts law. In this paper, we formalize the problem, and pursue algorithms for learning classifiers that are robust to gaming.},
language = {en},
urldate = {2026-01-20},
publisher = {arXiv},
author = {Hardt, Moritz and Megiddo, Nimrod and Papadimitriou, Christos and Wootters, Mary},
month = nov,
year = {2015},
note = {arXiv:1506.06980 [cs]},
keywords = {Computer Science - Machine Learning},
file = {PDF:/home/velocitatem/Zotero/storage/HNCDYGWS/Hardt et al. - 2015 - Strategic Classification.pdf:application/pdf},
}
@misc{liu_contextual_2024,
title = {Contextual {Dynamic} {Pricing} with {Strategic} {Buyers}},
url = {http://arxiv.org/abs/2307.04055},
doi = {10.48550/arXiv.2307.04055},
abstract = {Personalized pricing, which involves tailoring prices based on individual characteristics, is commonly used by firms to implement a consumer-specific pricing policy. In this process, buyers can also strategically manipulate their feature data to obtain a lower price, incurring certain manipulation costs. Such strategic behavior can hinder firms from maximizing their profits. In this paper, we study the contextual dynamic pricing problem with strategic buyers. The seller does not observe the buyer's true feature, but a manipulated feature according to buyers' strategic behavior. In addition, the seller does not observe the buyers' valuation of the product, but only a binary response indicating whether a sale happens or not. Recognizing these challenges, we propose a strategic dynamic pricing policy that incorporates the buyers' strategic behavior into the online learning to maximize the seller's cumulative revenue. We first prove that existing non-strategic pricing policies that neglect the buyers' strategic behavior result in a linear \$Ω(T)\$ regret with \$T\$ the total time horizon, indicating that these policies are not better than a random pricing policy. We then establish that our proposed policy achieves a sublinear regret upper bound of \$O({\textbackslash}sqrt\{T\})\$. Importantly, our policy is not a mere amalgamation of existing dynamic pricing policies and strategic behavior handling algorithms. Our policy can also accommodate the scenario when the marginal cost of manipulation is unknown in advance. To account for it, we simultaneously estimate the valuation parameter and the cost parameter in the online pricing policy, which is shown to also achieve an \$O({\textbackslash}sqrt\{T\})\$ regret bound. Extensive experiments support our theoretical developments and demonstrate the superior performance of our policy compared to other pricing policies that are unaware of the strategic behaviors.},
language = {en},
urldate = {2026-01-20},
publisher = {arXiv},
author = {Liu, Pangpang and Yang, Zhuoran and Wang, Zhaoran and Sun, Will Wei},
month = jun,
year = {2024},
note = {arXiv:2307.04055 [stat]},
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning, Computer Science - Computer Science and Game Theory, Computer Science - Artificial Intelligence},
file = {PDF:/home/velocitatem/Zotero/storage/MVJNULK3/Liu et al. - 2024 - Contextual Dynamic Pricing with Strategic Buyers.pdf:application/pdf},
}
@techreport{dhir_http_2025,
type = {Internet {Draft}},
title = {{HTTP} {Agent} {Profile} ({HAP}): {Authenticated} and {Monetized} {Agent} {Traffic} on the {Web}},
shorttitle = {{HTTP} {Agent} {Profile} ({HAP})},
url = {https://datatracker.ietf.org/doc/draft-dhir-http-agent-profile},
abstract = {Autonomous agents such as LLM-powered crawlers, browser-integrated assistants, and task-oriented bots are rapidly becoming first-class HTTP clients on the Web. Todays infrastructure largely assumes a human behind a browser and monetizes content through advertising and coarse subscriptions. Automated agents consume content at scale without rendering pages or viewing ads, exacerbating bot-mitigation arms races and economic misalignment between content providers and AI systems. This document describes an HTTP Agent Profile (HAP) that enables: (1) cryptographic authentication of agent traffic using HTTP Message Signatures; (2) clear separation between human and agent traffic using privacy-preserving human tokens; and (3) protocol-level value exchange for agents via HTTP status code 402 ("Payment Required") and pluggable micropayment mechanisms. The profile reuses existing HTTP features and is designed for incremental deployment via reverse proxies, CDNs, and agent libraries.},
number = {draft-dhir-http-agent-profile-00},
urldate = {2026-01-20},
institution = {Internet Engineering Task Force},
author = {Dhir, Sanat},
month = nov,
year = {2025},
note = {Num Pages: 13},
}
@misc{noauthor_amazoncom_2026,
title = {Amazon.com {Services} {LLC} v. {Perplexity} {AI}, {Inc}},
language = {en},
month = jan,
year = {2026},
note = {No. 3:25-cv-09514-MMC},
file = {PDF:/home/velocitatem/Zotero/storage/4JWZSTXJ/Posner - UNITED STATES DISTRICT COURT NORTHERN DISTRICT OF CALIFORNIA SAN FRANCISCO DIVISION.pdf:application/pdf},
}
@article{wright_2026_2025,
title = {2026 {Artificial} {Intelligence} {Outlook}: {The} {Great} {Competition} {Wars} {Have} {Begun}},
language = {en},
journal = {Pitchbook},
author = {Wright, Brian and Javaheri, Ali and Bellomo, Eric and Hernandez, Derek and Yang, Rudy and MacDonagh, John and DeGagne, Aaron and Frederick, Alex and Geurkink, Jonathan and Zabelin, Dimitri and Ulan, James},
month = dec,
year = {2025},
file = {PDF:/home/velocitatem/Zotero/storage/AIY5K3TX/Wright et al. - 2025 - Institutional Research Group.pdf:application/pdf},
}
@misc{rachitsky_marc_2026,
title = {Marc {Andreessen}: {The} real {AI} boom hasnt even started yet},
shorttitle = {Marc {Andreessen}},
url = {https://www.lennysnewsletter.com/p/marc-andreessen-the-real-ai-boom},
abstract = {On raising kids, why job loss fears are overblown, the future of PM/eng/design careers, and the macro force you should pay attention to},
language = {en},
urldate = {2026-02-01},
author = {Rachitsky, Lenny},
month = feb,
year = {2026},
file = {Snapshot:/home/velocitatem/Zotero/storage/DGW8PHMV/marc-andreessen-the-real-ai-boom.html:text/html},
}
@misc{noauthor_tpu_2025,
title = {{TPU} v6e},
url = {https://cloud.google.com/tpu/docs/v6e},
language = {es-419-x-mtfrom-en},
urldate = {2026-02-17},
journal = {Google Cloud Documentation},
month = dec,
year = {2025},
file = {Snapshot:/home/velocitatem/Zotero/storage/RNMB32KD/v6e.html:text/html},
}
@misc{noauthor_tpu_2025-1,
title = {{TPU} v5e {\textbar} {Google} {Cloud} {Documentation}},
url = {https://cloud.google.com/tpu/docs/v5e},
language = {es-419-x-mtfrom-en},
urldate = {2026-02-17},
month = dec,
year = {2025},
file = {Snapshot:/home/velocitatem/Zotero/storage/BLLG9NZC/v5e.html:text/html},
}
@misc{noauthor_tpu_2026,
title = {{TPU} v4 {\textbar} {Google} {Cloud} {Documentation}},
url = {https://cloud.google.com/tpu/docs/v4},
language = {es-419-x-mtfrom-en},
urldate = {2026-02-17},
month = feb,
year = {2026},
file = {Snapshot:/home/velocitatem/Zotero/storage/N724QGF6/v4.html:text/html},
}
@article{mann_test_1947,
title = {On a {Test} of {Whether} one of {Two} {Random} {Variables} is {Stochastically} {Larger} than the {Other}},
volume = {18},
url = {https://doi.org/10.1214/aoms/1177730491},
doi = {10.1214/aoms/1177730491},
abstract = {Let x and y be two random variables with continuous cumulative distribution functions f and g. A statistic U depending on the relative ranks of the x's and y's is proposed for testing the hypothesis f = g. Wilcoxon proposed an equivalent test in the Biometrics Bulletin, December, 1945, but gave only a few points of the distribution of his statistic. Under the hypothesis f = g the probability of obtaining a given U in a sample of n x's and m y's is the solution of a certain recurrence relation involving n and m. Using this recurrence relation tables have been computed giving the probability of U for samples up to n = m = 8. At this point the distribution is almost normal. From the recurrence relation explicit expressions for the mean, variance, and fourth moment are obtained. The 2rth moment is shown to have a certain form which enabled us to prove that the limit distribution is normal if m, n go to infinity in any arbitrary manner. The test is shown to be consistent with respect to the class of alternatives f(x) {\textgreater} g(x) for every x.},
number = {1},
journal = {The Annals of Mathematical Statistics},
author = {Mann, H. B. and Whitney, D. R.},
year = {1947},
note = {Publisher: Institute of Mathematical Statistics},
pages = {50 -- 60},
}

View File

@@ -10,27 +10,37 @@
In this paper we present an exploration and defense against the presence of new commercial entities in digitally powered platforms, preserving market equilibrium in the age of AI. This research establishes the following contributions: definition and formalization of non-human transactors in e-commerce platforms, development of a testing-ground for capturing the behavioral essence of these transactors across a large variety of digital systems, construction of a discriminative model (to prove separability) as a strong learner for downstream mitigation of contamination by non-human entities, translation of such learned separability into existing dynamic pricing machine learning loops, and finally establishment of a high-level KPI-affecting causal effect and cost-saving framework for the future of internet commerce in the presence of such non-human learners.
This research effort touches a large variety of domains, spanning behavioral economics for understanding the rationality of behavior as theorized by the concept of homo economicus, agent-based modeling to translate our learned separability into disjoint dynamic pricing systems, reinforcement learning which serves as the SOTA for price-learners, and dynamic pricing and market equilibrium theory to understand the risks of possible supra-competitive pricing phenomena in cases of adversarial pricing systems driving the market out of equilibrium.
This research effort touches a large variety of domains, spanning behavioral economics for understanding the rationality of behavior as theorized by the concept of homo economicus, agent-based modeling to translate our learned separability into disjoint dynamic pricing systems, reinforcement learning which serves as the SOTA for price-learners, and dynamic pricing and market equilibrium theory to understand the risks of possible supra-competitive pricing phenomena in cases of adversarial pricing systems driving the market out of equilibrium. \footnote{Given the rapid evolution of the field we acknowledge all developments with a cutoff set at the date of March 1st 2026.}
\subsection{Motivation and Market Context}
The current innovation boom in generative artificial intelligence and its applications to knowledge-based work tasks has brought many competing technologies for browser-use automation, with benchmarks and evaluations \cite{xia_evaluation-driven_2025} motivating the development of capabilities focused on commercial research, understanding, and transaction execution \cite{xie_osworld_nodate}. The ``AI Agent'' market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030. This surge reflects adoption in e-commerce, customer service, and enterprise automation, where agents handle interactions previously done by humans, raising the question of how these systems should be designed for future robustness as well as how to maintain a competitive edge in the analytical components of e-commerce platforms \cite{markntel_advisors_global_2025}.
The current innovation boom in generative artificial intelligence and its applications to knowledge-based work tasks has brought many competing technologies for browser-use automation, with benchmarks and evaluations \parencite{xia_evaluation-driven_2025} motivating the development of capabilities focused on commercial research, understanding, and transaction execution \parencite{xie_osworld_2024}. The ``AI Agent'' market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030. This surge reflects adoption in e-commerce, customer service, and enterprise automation, where agents handle interactions previously done by humans, raising the question of how these systems should be designed for future robustness as well as how to maintain a competitive edge in the analytical components of e-commerce platforms \parencite{markntel_advisors_global_2025}.
The key stakeholders affected by the threat of increasing agent-driven traffic include online businesses and platform operators (especially in bot-heavy sectors like retail, travel, and financial services), their security, fraud, and engineering teams, end users whose accounts and data are exposed and whose experience degrades, regulators and legal stakeholders responding to breaches and fraud, and the attackers or bot operators driving the automation \cite{imperva_rapid_2025}.
The key stakeholders affected by the threat of increasing agent-driven traffic include online businesses and platform operators (especially in bot-heavy sectors like retail, travel, and financial services), their security, fraud, and engineering teams, end users whose accounts and data are exposed and whose experience degrades, regulators and legal stakeholders responding to breaches and fraud, and the attackers or bot operators driving the automation \parencite{imperva_rapid_2025}.
The industry has already seen legal action in cases like Amazon against Perplexity \cite{ghaffary_amazon_nodate}, stemming from the difficulty of identifying traffic from hybrid systems like the Commet browser. This paper explores such systems to better understand what the interaction data looks like and what it means for dynamic pricing and recommendation systems downstream. This observed impact indicates a need for prevention of secondary negative effects on the ``legacy'' systems which power modern revenue sources for many companies. Dynamic pricing algorithms rely on directly translating demand features $q$ to new price assignments $\hat{p}$ across a catalogue of products of size $N$. This opens opportunities to design a \textit{tabula rasa} of digital market mechanisms that will shape the future of commerce in the age of artificial intelligence.
The industry has already seen legal action in cases like Amazon against Perplexity \parencite{ghaffary_amazon_2025}, stemming from the difficulty of identifying traffic from hybrid systems like the Commet browser. This paper explores such systems to better understand what the interaction data looks like and what it means for dynamic pricing and recommendation systems downstream. This observed impact indicates a need for prevention of secondary negative effects on the ``legacy'' systems which power modern revenue sources for many companies. Dynamic pricing algorithms rely on directly translating demand features $q$ to new price assignments $\hat{p}$ across a catalogue of products of size $N$. This opens opportunities to design a \textit{tabula rasa} of digital market mechanisms that will shape the future of commerce in the age of artificial intelligence.
\subsection{Solution Space Overview}
Dynamic pricing systems, as presented in \cite{mueller_low-rank_2019}, often deal with sparse low-rank data of demand signals which, combined with contamination from agents, creates complex interactions that impact pricing. To further complicate the problem, certain commercial settings such as the one presented in \cite{amjad_censored_2017} must address the true demand of products under censored observations. This provides a formulation for handling demand in our case with multiple kinds of commercial mediators: $\hat{q} \gets q_A + q_H$ where $q_A$ represents the distribution of demand generated by agentic mediators and $q_H$ represents that of true human demand, these are two distinct populations with divergent objective functions.
Dynamic pricing systems, as presented by \textcite{mueller_low-rank_2019}, often deal with sparse low-rank data of demand signals which, combined with contamination from agents, creates complex interactions that impact pricing. To further complicate the problem, certain commercial settings such as the one presented by \textcite{amjad_censored_2017} must address the true demand of products under censored observations. This provides a formulation for handling demand in our case with multiple kinds of commercial mediators: $\hat{q} \gets q_A + q_H$ where $q_A$ represents the distribution of demand generated by agentic mediators and $q_H$ represents that of true human demand, these are two distinct populations with divergent objective functions.
We formally define interaction data as coming from some actor which can either be an agent ($A$) or human ($H$). For purposes of this research, an agent is an algorithmic loop with the ability to access a web platform and perform actions such as clicks, scrolls, and input field fills. The loop terminates when the internal large language model judges the provided task definition as complete. A detailed breakdown can be found in \cref{algagent-loop}.
\subsection{Research Questions}
This dissertation is organized around one main research question and three supporting sub-questions:
\begin{enumerate}
\item[\textbf{Main RQ}] How can dynamic pricing systems preserve margin integrity when transaction orchestration is increasingly mediated by non-human agents?
\item[\textbf{SQ1}] \textit{Separability}: Can agent and human sessions be reliably distinguished from behavioral interaction signals alone, without relying on network-level or device fingerprinting?
\item[\textbf{SQ2}] \textit{Theoretical Impact}: What is the formal relationship between agent contamination levels and the erosion of pricing power in dynamic pricing systems?
\item[\textbf{SQ3}] \textit{Robust Mitigation}: How can pricing policies be constructed to maintain margin integrity under unknown and non-stationary levels of agent contamination?
\end{enumerate}
\begin{algorithm}[t]
\DontPrintSemicolon
\SetKwInOut{Input}{Input}
\SetKwInOut{Output}{Output}
\SetKwInput{Input}{Input}
\SetKwInput{Output}{Output}
\Input{Goal $G$, Platform URL $u$, LLM $\mathcal{M}$}
\Output{Task completion result $r$}
@@ -54,4 +64,4 @@ Extract final result $r$ from terminal state\;
\end{algorithm}
The previously described goal of separability allows us to formulate a task which entails taking raw interaction data for either actor and creating a composite demand estimate $\hat{q}$. We propose a robust optimization objective defined in our methodology, transforming the pricing problem into a form of Distributionally Robust Optimization \cite{kuhn_distributionally_2025} where the learner must guard against adversarial contamination in observed demand distributors. In this setting we must learn to make decision that perform under the assumption of not having a single estimated probability distribution but under an ambiguity set of any distribution, of which we have limited information. In our case as stated is a mixture of distributions with a parameter which is unknown and non-stationary.
The previously described goal of separability allows us to formulate a task which entails taking raw interaction data for either actor and creating a composite demand estimate $\hat{q}$. We propose a robust optimization objective defined in our methodology, transforming the pricing problem into a form of Distributionally Robust Optimization \parencite{kuhn_distributionally_2025} where the learner must guard against adversarial contamination in observed demand distributors. In this setting we must learn to make decision that perform under the assumption of not having a single estimated probability distribution but under an ambiguity set of any distribution, of which we have limited information. In our case as stated is a mixture of distributions with a parameter which is unknown and non-stationary.

View File

@@ -1,28 +1,29 @@
\section{Literature Review}
To better understand all wedges of the work, we must start by exploring the nature of agents and agentic computer use and web automation, complementing that with economic reasoning and strategic interaction. The final surface to cover, leads us to data-driven dynamic pricing under uncertainty. The key technical risk is not ``agents buying things'' per se, but agents shaping the behavioral and demand signals that downstream pricing systems consume and depend on. The introduction of these mediating actor entities into economic systems, is further creating a threat of false-name bidding \cite{yokoo_effect_2004}, which prior research has explored in a trading context. Other research on pseudonyms in dynamic systems, demonstrate whitewashing in AI agents which can ignore defensive mechanisms by re-entry with different identities \cite{feldman_free-riding_2004}. Dynamic pricing assumes demand proxies are behaviorally meaningful, while bot detection aims at security and access control. The missing bridge is a principled framework for separating non-human reconnaissance from genuine human demand expression and integrating that separation into pricing heuristics without degrading legitimate user experience (in our research tracked by the user-experience index). This gap, is what our contribution aims to address, particularly for the aforementioned stakeholder groups.
To better understand all wedges of the current works, we must start by exploring the nature of agents, agentic computer use and web automation, complementing that with economic reasoning and strategic interaction. The final surface to cover, leads us to data-driven dynamic pricing under uncertainty. The key technical risk is not ``agents buying things'' per se, but agents shaping the behavioral and demand signals that downstream pricing systems consume and depend on. This latter case of agents shopping is currently pending legal action in the case of \textcite{noauthor_amazoncom_2026} which is currently being treated as a violation of the Computer Fraud and Abuse Act. The introduction of these mediating actor entities into economic systems, is further creating a threat of false-name bidding \parencite{yokoo_effect_2004}, which prior research has explored in a trading context. Other research on pseudonyms in dynamic systems, demonstrate whitewashing in AI agents which can ignore defensive mechanisms by re-entry with different identities \parencite{feldman_free-riding_2004}. Dynamic pricing assumes demand proxies are behaviorally meaningful, while bot detection aims at security and access control. The missing bridge is a principled framework for separating non-human reconnaissance from genuine human demand expression and integrating that separation into pricing heuristics without degrading legitimate user experience (in our research tracked by the user-experience index). This gap, is what our contribution aims to address, particularly for the aforementioned stakeholder groups.
\subsection{Agent Taxonomy and Definitions}
An agent in the context of artificial intelligence is generally defined by anything that can reason and act upon observations of its environments (collected through some sensory inputs) and carry out actions through effectors. Moreover, a rational agent is an entity that is capable of perceiving the world around them and taking actions to advance specified goals. This definition by \cite{russell_artificial_nodate} is further developed in an economic context by \cite{parkes_economic_2015}, suggesting AI research attempts to construct a synthetic \textit{homo economicus}, which may also be termed \textit{machina economicus}.
A specific class or taxon of this \textit{machina economicus}, the Large Language Model (LLM) agent, is defined as an autonomous system capable of achieving goals and adapting post-training, often without needing explicit code or fundamental model changes. \cite{xia_evaluation-driven_2025}
An agent in the context of artificial intelligence is generally defined by anything that can reason and act upon observations of its environments (collected through some sensory inputs) and carry out actions through effectors. Moreover, a rational agent is an entity that is capable of perceiving the world around them and taking actions to advance specified goals. This definition by \textcite{russell_artificial_2021} is further developed in an economic context by \textcite{parkes_economic_2015}, suggesting AI research attempts to construct a synthetic \textit{homo economicus}, which may also be termed \textit{machina economicus}.
A specific class or taxon of this \textit{machina economicus}, the Large Language Model (LLM) agent, is defined as an autonomous system capable of achieving goals and adapting post-training, often without needing explicit code or fundamental model changes \parencite{xia_evaluation-driven_2025}.
We must however acknowledge the current SOTA as presented by OSWORLD simulations in \cite{xie_osworld_nodate} have demonstrated that multi-modal tasks across desktop and web interaction modes, have a top-performing score of only 12.24\% success, whereas humans have a higher 72\% success rate. This weakness matters for this research because it clarifies the near-term threat model: practical exploitation does not require a fully competent ``computer assistant'', only enough automation to perform high-volume reconnaissance actions (search/filter/open product pages, probe availability/price boundaries) that can contaminate behavioral signals. With the expected growth of these capabilities, this threat only becomes more perilous to revenue management systems.
We must however acknowledge the current SOTA as presented by OSWORLD simulations by \textcite{xie_osworld_2024} have demonstrated that multi-modal tasks across desktop and web interaction modes, have a top-performing score of only 12.24\% success, whereas humans have a higher 72\% success rate; this is linked to the lack of grounding of these agents and their inability of handling unexpected errors. This weakness matters for this research because it clarifies the near-term threat model: practical exploitation does not require a fully competent ``computer assistant'', only enough automation to perform high-volume reconnaissance actions (search/filter/open product pages, probe availability/price boundaries) that can contaminate behavioral signals. With the expected growth of these capabilities, this threat only becomes more perilous to revenue management systems.
We model an agent session as producing some events with lower in-session conversion levels relative to humans, this we state in our assumption that $P(\text{purchase} \vert A) \ll P(\text{purchase} \vert H)$ but with a potentially higher volatility in $\hat{q}$, which we observe through the look-to-book metrics in our simulation.
We model an agent session as producing some events with lower in-session conversion levels relative to humans, this we state in our assumption that $P(\text{purchase} \vert A) < P(\text{purchase} \vert H)$ but with a potentially higher volatility in $\hat{q}$, which we observe through the look-to-book metrics in our simulation.
\subsection{Economic Agents: From Homo Economicus to Machina Economicus}
Existing behavioral economic models tend to be criticized for the assumption of rational behavior, as is embodied in the term of homo economicus. The definition of a machina economicus by \cite{parkes_economic_2015} is quite appropriate for our case, particularly because these assumptions of rationality have been argued to be a very adequate reference for AI research by \cite{varian_economic_1995}. For modeling this behavior, the trajectories of these agents can be formally defined to be partially observable Markov decision processes. \cite{xie_osworld_nodate} Agents are however not to be confused with web-bots which have previously been known as automated software applications or scrapers which are set with a purpose of carrying out specific tasks on the internet, without a higher level of internal judgement. \cite{imperva_rapid_2025} In our research, we refer to this actor simply as an Agent belonging to the distribution $A$.
Existing behavioral economic models tend to be criticized for the assumption of rational behavior, as is embodied in the term of homo economicus. The definition of a machina economicus by \textcite{parkes_economic_2015} is quite appropriate for our case, particularly because these assumptions of rationality have been argued to be a very adequate reference for AI research by \textcite{varian_economic_1995} due to its expected utility maximizing nature. For modeling this behavior, the trajectories of these agents can be formally defined to be partially observable Markov decision processes \parencite{xie_osworld_2024}. Agents are however not to be confused with web-bots which have previously been known as automated software applications or scrapers which are set with a purpose of carrying out specific tasks on the internet, without a higher level of internal judgement \parencite{imperva_rapid_2025}. In our research, we refer to this actor simply as an Agent belonging to the distribution $A$.
This economic framing also helps separate two related but distinct phenomena of agents as buyers (changing market demand composition), and agents as information gatherers (changing the observed interactions used by pricing/recommendation systems). The thesis focuses on the second, where information acquisition strategically precedes purchase execution. We do not however dismiss the proposed expectation that existing economic systems serving humans, will not be populated by AIs across multiple channels and with various possibly misaligned goals as stated by \cite{parkes_economic_2015}.
This economic framing also helps separate two related but distinct phenomena of agents as buyers (changing market demand composition), and agents as information gatherers (changing the observed interactions used by pricing/recommendation systems). The thesis focuses on the second, where information acquisition strategically precedes purchase execution. We do not however dismiss the proposed expectation that existing economic systems serving humans, will not be populated by AIs across multiple channels and with various possibly misaligned goals as stated by \textcite{parkes_economic_2015}.
A HAP (HTTP Agent Profile) protocol has been developed as an internet draft by \textcite{dhir_http_2025} in an effort to separate agentic and human internet traffic, however the majority adoption by both the sellers and agent providers would be required for the implementation of such a solution.
\subsection{Problem Evidence and Market Impact}
The statistical issue of contamination in dynamic pricing systems that observe demand features as a means to update prices has been documented in various previous contexts. The airline industry (which has accounted for 24\% of observed disruptions) has seen malicious activity with a measureable impact on skewing key performance indicators by behavior visible in the look-to-book metrics. Excessive reconnaissance traffic inflates search volume without corresponding completed bookings, thereby skewing demand forecasts and disrupting dynamic pricing models. Demand proxies have also been observed to cause significant threat to inventory management by creating artificial scarcity that distorts the demand-supply relationships in the enterprise model. Censored demand as shown in \cite{amjad_censored_2017} can also be observed in low-bias demand under-estimation caused by a distortion effect coming from non-human traffic data. \cite{imperva_rapid_2025}
The statistical issue of contamination in dynamic pricing systems that observe demand features as a means to update prices has been documented in various previous contexts. The airline industry (which has accounted for 24\% of observed disruptions) has seen malicious activity with a measureable impact on skewing key performance indicators by behavior visible in the look-to-book metrics. Excessive reconnaissance traffic inflates search volume without corresponding completed bookings, thereby skewing demand forecasts and disrupting dynamic pricing models. Demand proxies have also been observed to cause significant threat to inventory management by creating artificial scarcity that distorts the demand-supply relationships in the enterprise model. Censored demand as shown by \textcite{amjad_censored_2017} can also be observed in low-bias demand under-estimation caused by a distortion effect coming from non-human traffic data \parencite{imperva_rapid_2025}.
When dynamic pricing algorithms operate on highly contaminated or noisy data, the risk grows significantly in creating inaccurate price inferences. The emergent mitigation driven by un-informed reward and regret signals might lead to price suppression for sales continuity which results in harming margins and resulting in a revenue loss. System that poorly fit undesired behavior might result in price gouging, which calls for strong guardrails while preserving targeted business strategy. \cite{mullapudi_reinforcement_nodate}
When dynamic pricing algorithms operate on highly contaminated or noisy data, the risk grows significantly in creating inaccurate price inferences. The emergent mitigation driven by un-informed reward and regret signals might lead to price suppression for sales continuity which results in harming margins and resulting in a revenue loss. System that poorly fit undesired behavior might result in price gouging, which calls for strong guardrails while preserving targeted business strategy \parencite{mullapudi_reinforcement_2025}.
%Documented instances of agent-driven market disruptions - Quantitative evidence of pricing manipulation - Case studies from affected industries
@@ -30,17 +31,42 @@ When dynamic pricing algorithms operate on highly contaminated or noisy data, th
\subsection{Theoretical Foundations: Economic Parallels}
Early hints of exploration of prices in a standard English auction explored by \textcite{varian_economic_1995} which hints at exploration of prices in a sequential manner, which leads to a marginally different cost to the bidder than the reservation price of the seller. This is a setting in which there is no cost incured by the buyer for their actions or exploring prices in the market. They propose that any agent responsable for the pricing of a good must be imune to dynamic strategies which might extract private information from a market. A key take-away which relates to the Vickery auction mechanism (also called a \textit{direct mechanism}) suggests that not only would defenses against such exploitation be necessary, but the construction of a mechanism in which revelation of the true willingness to pay is the dominant strategy for commerce.
Early hints of exploration of prices in a standard English auction explored in \cite{varian_economic_1995} which hints at exploration of prices in a sequential manner, which leads to a marginally different cost to the bidder than the reservation price of the seller. This is a setting in which there is no cost incured by the buyer for their actions or exploring prices in the market. They propose that any agent responsable for the pricing of a good must be imune to dynamic strategies which might extract private information from a market. A key take-away which relates to the Vickery auction mechanism (also called a \textit{direct mechanism}) suggests that not only would defenses against such exploitation be necessary, but the construction of a mechanism in which revelation of the true willingness to pay is the dominant strategy for commerce.
Like in classical revenue-maximizing auctions \parencite{roughgarden_cs364a_2013} we assume that the human actor in our system has a private valuation $v$ which we formally draw from intrinsically defined distributions. The important note here is that the agent proxy does not have a mechanism to convey this private information into the demand data which directly impacts the pricing systems.
Like in classical revenue-maximizing auctions \cite{roughgarden_cs364a_2013} we assume that the human actor in our system has a private valuation $v$ which we formally draw from later defined distributions. The important note here is that the agent proxy does not have a mechanism to convey this private information into the demand data which directly impacts the pricing systems.
The key component of this mediation between agents and commercial platforms lays in the transaction costs related to information gathering and negotiation. As proposed by \textcite{shahidi_coasean_2025} these costs are bound to collapse towards zero (which we demonstrate mathematically), calling for a re-evaluation of the boundaries between firms and markets. As argued by \textcite{coase_nature_1937}, the market participation and time associated with that participation, is critical part of the Coasean transaction cost logic which includes the discovery or relevant pricing within a given market. This process of price discovery without the presence of AI Agents can be time consuming and resource intensive. To build on top of this work we provide a proof of optimal conditions theorised by Coaes as an extension to AI-mediated markets.
% Economic foundations: relating the problem to options pricing theory. Cost of Information (COI) concept and its relevance
% Link Coasean Singularity and other economic market theory and highlight specific information of supra competitive pricing.
\subsection{Landscape of Existing Work}
Previous efforts in adversarial computer use LLM agents, show how multi-faceted the whole problem is
Here we can show a market visualization (venn-like-diagram)
Explorations of the algorithmic collusion by LLMs \parencite{fish_algorithmic_2025} has demonstrated a cross-model tendency of market division with a strong sensitivity to instructions provided in the ``system prompt''. If a dynamic pricing algorithm which is trained to respond to market signals learns to coordinate with competitor agents (or become manipulated by those agents), the market equilibrium is under threat of destabilization. This is particularly true for Q-learning pricing learners as demonstrated by \textcite{calvano_artificial_2018}.
Our effort to combat contamination stems from research by \textcite{hardt_strategic_2015} on strategic classification, in conjunction with \textcite{liu_contextual_2024} who demonstrate a linear regret if contamination is ignored. The strategic classification adversarial effect comes from an effort to manipulate some representative features used in a learning pipeline, which can result in lower prices on loans or lower prices from dynamic pricing algorithms.
To bridge the gap between detection and robust pricing, we look at work in Distributionally Robust Optimization (DRO). As defined by \textcite{kuhn_wasserstein_2024}, DRO provides a framework for decision-making under ambiguity, where the true data distribution is unknown but lies within a ``Wasserstein ball'' of a target distribution. In our context, the ``ambiguity set'' represents the uncertainty introduced by agentic reconnaissance. By optimizing for the worst-case distribution within this set, pricing mechanisms can become resilient to the distributional shifts such as the ones caused by non-human actors, effectively robustifying the revenue function against the contamination described in our problem statement.
In order to create an environment in which prices can be tested against a demand estimate generated by some behavioral model, we take inspiration from the architecture proposed by \textcite{ie_recsim_2019} in the RecSim platform built for recommendation systems. By modeling the distinct user behavior as POMDPs we can generate faithful interactions which allow us to generalize, past the constraint which is also present in recommendation systems, of rarely having enough experience with individual actor's interactions for good recommendations without generalization. The key inspiration comes from the user choice modeling which we translate to a user transition model for each distinct actor type (agent or human). We further consider the possibility of modeling our quantitative research platform using dynamic Bayesian networks for the sake of tractability within the system. The contribution or RecSim enables researchers to better understand learning algorithms in fixed environments, a gap we identify as needing to be bridged within the space of dynamic pricing.
% TODO: mention https://github.com/meta-pytorch/OpenEnv/tree/main/envs/browsergym_env
We also acknowledge the difficulty in similarly affected fields such as authorship, where \textcite{ganie_uncertainty_2025} demonstrate the theoretical limits of the distributional divergence between text authored by a human or large language model. Their approach of computing the divergence between two distributions demonstrates purely theoretically that no classifier can outperform random guessing on their particular task. This is yet another factor to take into consideration when exploring the potential mitigation strategies.
The setting of our work is quite complex and covers a wide range of topics, each with its own set of issues that further complicate the task at hand. There is however promise in the field of reinforcement learning and adversarial robustness to combat these problems. We can summarize the characteristics learned from the review of our environment as:
\begin{enumerate*}[label=(\roman*)]
\item non-stationary demand with temporal noise $\epsilon_t$
\item contaminated behavioral signals from mixed human-agent traffic with unknown mixing ratio $\alpha$
\item partial observability where only demand proxies $\hat{q}$ are available, not true demand $d(\cdot)$
\item strategic actors capable of feature manipulation to influence pricing outcomes
\item information asymmetry with private valuations $v$ drawn from unknown distributions
\item session-based interactions modeled as POMDPs with trajectories $\tau_s$
\item low conversion probability for agents: $P(\text{purchase} \mid A) < P(\text{purchase} \mid H)$
\item distributional uncertainty requiring robust optimization within Wasserstein ambiguity sets
\item potential for adversarial exploitation through false-name bidding and identity whitewashing.
\end{enumerate*}
%Previous efforts in adversarial computer use .LLM agents, show how multi-faceted the whole problem is
%Here we can show a market visualization (venn-like-diagram)

View File

@@ -1,10 +1,13 @@
\section{Methodology}
% Extra notes and clarifications: we observed some humans and get their transition probabilities between event types
% We modify behavioral profiles of transition matrices with price elasticity matrices generated by sample valuations of a distributing.
This section details the theoretical and practical framework developed to address dynamic pricing under the influence of non-human actors. We begin by formalizing the problem environment and the nature of the actors. We then derive the \textit{Cost of Information} (COI) theorem, proving the erosion of pricing power in the limit of agent saturation. Following this, we outline our generative contamination strategy using GOFAI-driven separability and transition probability learning. Finally, we formulate the robust control problem as a Stackelberg game solved via Distributionally Robust Reinforcement Learning (DR-RL) with constructed ambiguity sets.
\subsection{Problem Formalization}
We define a commercial environment where the platform interacts with a stream of sessions. Let $\mathcal{S}$ denote the set of all sessions. Each session $s \in \mathcal{S}$ is generated by an actor belonging to a latent class $Y_s \in \{H, A\}$, where $H$ denotes Human and $A$ denotes Agent.
We define a commercial environment where the platform interacts with a stream of sessions. Let $\mathcal{S}$ denote the set of all sessions. Each session $s \in \mathcal{S}$ is generated by an actor belonging to a latent class $\theta_s \in \{H, A\}$, where $H$ denotes Human and $A$ denotes Agent.
Each session produces a trajectory of observable events $\tau_s = (e_{s,1}, \ldots, e_{s,L_s})$. An event $e_{s,k}$ is a tuple defined as:
\begin{equation}
@@ -19,13 +22,21 @@ where:
The platform does not directly observe the true underlying demand function $d(p)$. Instead, it observes a behavioral proxy $\hat{q}_t$, which is a composite signal derived from the mixture of actor types. We define the demand proxy for product $i$ at epoch $t$ as a weighted aggregation of events:
\begin{equation}
\label{eq:qhat}
\hat{q}_{t,i} = \sum_{s \in \mathcal{S}_t} \sum_{k=1}^{L_s} \omega(a_{s,k}) \cdot \mathbb{1}[i_{s,k} = i]
\end{equation}
where $\omega: \mathcal{A} \to \mathbb{R}_+$ assigns weights to actions based on their signal strength regarding willingness to pay.
\subsubsection{Actor Types and Demand Curves}
We formalize the heterogeneity of actors by introducing a type space $\Theta$. An actor of class $Y_s$ is further parameterized by a type $\theta \sim \mathcal{D}_{Y}$. This type determines the actor's demand response function $d(p; \theta)$, sampled from a distribution of possible demand curves. The total observed demand is a stochastic process governed by the mixture:
In the current engine implementation, we use the normalized variant of this proxy for each step:
\begin{equation}
\tilde q_{t,i} = 100 \cdot \frac{\hat q_{t,i}}{\sum_{j=1}^{N}\hat q_{t,j} + \varepsilon}
\end{equation}
with fixed category-level weights (cart, dwell, nav, filter) following the same rank order from Table~\ref{tab:action_space}. This keeps the signal dense and directly usable in the simulator.
\subsubsection{Actor Types and Demand Curves}
We formalize the heterogeneity of actors by introducing a type space $\Theta$. An actor of class $Y_s$ is further parameterized by a type $\theta \sim \mathcal{D}_{Y}$. This type determines the actor's demand response function $d(p; \theta)$, sampled from a distribution of possible demand curves. The total observed demand is a stochastic process governed by the naively defined mixture:
\begin{equation}
\label{eq:mixture_demand}
Q(p) = (1-\alpha) \cdot \mathbb{E}_{\theta \sim \mathcal{D}_H}[d(p; \theta)] + \alpha \cdot \mathbb{E}_{\theta \sim \mathcal{D}_A}[d(p; \theta)] + \epsilon_t
\end{equation}
where $\alpha \in [0, 1]$ represents the contamination parameter (proportion of agents) and $\epsilon_t$ is non-stationary market noise.
@@ -34,15 +45,18 @@ where $\alpha \in [0, 1]$ represents the contamination parameter (proportion of
\subsection{Cost of Information (COI) Framework}
The \textit{Cost of Information} (COI) represents the markup a pricing policy $\pi$ attempts to extract from the market by leveraging demand signals. We define COI as the expected premium over the minimum viable price $\underline{p}$ (or marginal cost). This also speaks to the financial urgency as a consequence of information asymmetry between the platform and the actors.
The platform's pricing power comes from information asymmetry: users who express strong interest signals pay more than the base price. We quantify this markup as the \textit{Cost of Information} (COI), which represents the average premium extracted above marginal cost. COI measures the revenue at risk when information asymmetry collapses.
A top-level view in the current AI discourse is that sufficiently large productivity gains can induce vertical deflation through cost compression and supply expansion \parencite{rachitsky_marc_2026}. Our contribution is narrower and mechanism-level: even under long-run deflation, platform revenue still depends on short-run information costs to the user. We formalize that rent as the Cost of Information (COI) and study how agentic reconnaissance accelerates its erosion.
\begin{definition}[Cost of Information]
Let $\pi(\tau)$ be a pricing policy mapping interaction histories to prices. The COI is defined as:
\begin{align}
\text{COI} &= \mathbb{E}[P] - \underline{p} \\
&= \int_{\underline{p}}^{\bar{p}} (1 - F_\pi(p)) \, dp
\end{align}
where $F_\pi(p)$ is the cumulative distribution function of prices generated by $\pi$ under standard operating conditions.
\begin{equation}
\text{COI} = \mathbb{E}[P] - \underline{p}
\end{equation}
where $\mathbb{E}[P]$ is the expected price charged by the policy and $\underline{p}$ is the minimum viable price (marginal cost).
% Alternative survival function representation (used in proof):
% COI = \int_{\underline{p}}^{\bar{p}} (1 - F_\pi(p)) \, dp
% where F_\pi(p) is the CDF of prices generated by \pi
\end{definition}
\begin{figure}[ht]
@@ -79,46 +93,39 @@ where $F_\pi(p)$ is the cumulative distribution function of prices generated by
We now formally demonstrate that standard dynamic pricing mechanisms are not incentive-compatible with high-frequency agentic traffic. As the number of independent competitive agents $N$ querying the system grows, the platform's ability to sustain a COI vanishes.
A fundamental assumption for our claim lies in the alignment of the AI agent through its prompt which has been demonstrated by \cite{fish_algorithmic_2025} to cause strong collusive behavior under linguistic nudges. This assumption can be generalized to the human user asking the agent to research products with a minimizing objective.
\begin{theorem}[COI Erosion in the Limit]
Let $N$ be the number of independent, utility-maximizing agents querying the platform. Let $p_{(1)}$ be the first order statistic (minimum) of the prices offered to these agents. As $N \to \infty$, the Cost of Information converges to 0.
\end{theorem}
\begin{proof}
Let $p_1, \ldots, p_N$ be independent and identically distributed (i.i.d.) price samples drawn from the policy's distribution $F(p)$ with support $[\underline{p}, \bar{p}]$. The realizable price for an optimal searching agent is the first order statistic $p_{(1)} = \min(p_1, \ldots, p_N)$.
The survival function (or reliability function) of the minimum price is given by:
Consider $N$ independent agents querying the platform, each receiving a price sample $p_i$ drawn from the pricing policy's distribution $F(p)$ bounded by $[\underline{p}, \bar{p}]$. A strategic agent conducting reconnaissance will select the minimum observed price: $p_{(1)} = \min(p_1, \ldots, p_N)$.
% support here means that its the range of possible outputs.
The probability that the minimum price exceeds some threshold $t$ is:
\begin{equation}
S_{p_{(1)}}(t) = P(p_{(1)} > t) = [1 - F(t)]^N
P(p_{(1)} > t) = P(\text{all } p_i > t) = [1 - F(t)]^N
\end{equation}
To determine the expected value $\mathbb{E}[p_{(1)}]$, we recall the property that for any continuous random variable $X$ with support $[A, B]$, the expectation can be expressed as the lower bound plus the integral of the survival function:
For any price $t > \underline{p}$, the CDF satisfies $F(t) > 0$, so $1 - F(t) < 1$. As $N$ grows, this probability decays exponentially: $[1 - F(t)]^N \to 0$.
The expected minimum price can be written as:
\begin{equation}
\mathbb{E}[X] = A + \int_{A}^{B} P(X > t) \, dt
\mathbb{E}[p_{(1)}] = \underline{p} + \int_{\underline{p}}^{\bar{p}} [1 - F(t)]^N \, dt
\end{equation}
Applying this to our pricing statistic where the lower bound is $\underline{p}$:
\begin{align}
\mathbb{E}[p_{(1)}] &= \underline{p} + \int_{\underline{p}}^{\bar{p}} P(p_{(1)} > t) \, dt \\
&= \underline{p} + \int_{\underline{p}}^{\bar{p}} [1 - F(t)]^N \, dt
\end{align}
Since $F(t)$ is a valid CDF, for any $t > \underline{p}$, we have strict inequality $F(t) > 0$, implying $0 \le 1 - F(t) < 1$. By the properties of limits, as $N \to \infty$, the term $[1 - F(t)]^N$ converges to 0 pointwise for all $t > \underline{p}$.
Applying the Lebesgue Dominated Convergence Theorem (noting that the integrand is bounded by 1 on the finite interval $[\underline{p}, \bar{p}]$):
Since the integrand vanishes as $N \to \infty$ for all $t > \underline{p}$, the integral converges to zero. Therefore:
\begin{equation}
\lim_{N \to \infty} \int_{\underline{p}}^{\bar{p}} [1 - F(t)]^N \, dt = \int_{\underline{p}}^{\bar{p}} 0 \, dt = 0
\lim_{N \to \infty} \text{COI} = \lim_{N \to \infty} (\mathbb{E}[p_{(1)}] - \underline{p}) = 0
\end{equation}
Substituting this back into the expression for COI:
\begin{align}
\lim_{N \to \infty} \text{COI} &= \lim_{N \to \infty} (\mathbb{E}[p_{(1)}] - \underline{p}) \\
&= \lim_{N \to \infty} \left( (\underline{p} + 0) - \underline{p} \right) \\
&= 0
\end{align}
\end{proof}
This result proves that standard pricing policies $\pi$ fail to extract surplus in the presence of large-scale agentic search, necessitating a robust counter-mechanism.
This result naively proves that standard pricing policies $\pi$ fail to extract surplus in the presence of large-scale agentic search, necessitating a robust counter-mechanism.
% The DRO objective creates a lower bound on COI extraction, effectively guaranteeing a minimum margin even in the presence of adversarial agents. we need to prove this and demonstrate that in a theorem.
@@ -129,15 +136,22 @@ This result proves that standard pricing policies $\pi$ fail to extract surplus
In order for our research to have grounding in interactions we built a robust e-commerce web-platform. We initially conducted a survey of the leading platforms of airlines and hotel booking sites to identify the specific interface patterns that effectively manage complex travel data. Our analysis revealed a clear industry standard: while both sectors rely on tabbed service selection and left-sidebar filtering to streamline navigation, they diverge in result presentation: airlines utilize visual date-price bars and multi-step wizards to optimize for logistical transparency, whereas hotel platforms leverage image-led cards and scarcity triggers to drive emotional engagement and urgency. Our web framework defines a highly agnostic boilerplate which can be seeded with any data-modality with an easy-to-tailor pattern, which we leverage to define a \texttt{hotel} and \texttt{airline} mode. Both modes are then individually deployed via an environment level argument which adjusts the proxy routing with a custom middleware inside next.js to render only the desired mode. The purpose of this was to create a baseline adaptable to any use-case or desired commercial application.
The architecture of this platform begins with the deployed web-apps posting interaction data to our backend which processes them and stores each ingested interaction into a kafka cluster. This serves as our data reservoir tracking and associating each interaction with its session and importantly with which experiment it belongs to. Not only do we track the behavioral interactions, but our pricing provider micro-service, once called by the frontend reports the observed/queried price-product into kafka. This kafka cluster is subscribed to by our pipeline which is configured on a schedule in Airflow, with the possibility of manual trigger. The final stage of the pricing pipeline, submits computed dynamic pricing results into a redis database for quick updates which is then read by the pricing provider and displayed on the webapp. This is a very generic end-to-end mechanism which is applicable to a variety of different e-commerce tasks. We intentionally put emphasis on the development of this infrastructure to establish a reproducible framework for interaction and to minimize any noise.
\paragraph{Public Web Artifact} We transition the Kappa like architecture of the data collection to a Lambda architecture for actual learning in a surrogate environment. This allows us to move faster on data which is provided and helps us create a feedback loop for production deployment. To support further research in this intersection of fields we release P4P \footnote{\url{https://github.com/velocitatem/p4p}} as a public repository providing the interaction layer of the PHANTOM framework. This provides a configurable storefront which can be tailored to any commercial setting with a standardized session-level event tracking. We document the API adapters or what the framework expects in terms of schemas for pricing providers and log ingestion servicse. The repository is intended for controlled experimentation and method replication rather than production commerce deployment.
\subsubsection{DevOps Principles}
Reproducible results are key to quality research platforms, this is taken into mind when deploying and working with our research platform. From a deployment standpoint the platform can be deployed across a large variety of providers and can be run locally. When developing a new interaction modality apart from the ones that come out of the box, a simple template pattern can be followed. The middleware of the framework is designed to properly render the chosen modality from environmental variables, thus deployment of different or parallel version of the software can be easily parametrized.
\subsubsection{Online Dynamic Pricing}
The dynamic pricing done is handled by a pipeline which computes a demand estimate on a per-product basis of a specific window of the data, defined by the period $T$ which by default is 5 minutes. This dynamic pricing pipeline computes a demand estimate vector $\hat{q} \in \mathbb{R}^N$ by a weighted sum of interactions for each product, it additionally computes a price elasticity vector $\hat{\epsilon}$ in the same dimensions as our demand. The final features matrix is of the size $N \times 2$ which we translate to a new price vector $\hat{p} \in \mathbb{R}^N$. The transformation that governs this dynamic pricing is a very simple surge-based pricing (a special case of our later defined policy $\pi$):
In order to collect data from actors under correct conditions we replicate a naive and simple dynamic pricing algorithm which runs in the background during the experiments.
The dynamic pricing done is handled by a pipeline which computes a demand estimate on a per-product basis of a specific window of the data, defined by the period $T$ which by default is 5 minutes. This dynamic pricing pipeline computes a demand estimate vector $\hat{q} \in \mathbb{R}^N$ by a weighted sum of interactions for each product, it additionally computes a price elasticity vector $\hat{\epsilon}$ in the same dimensions as our demand. The final features matrix is of the size $N \times 2$ which we translate to a new price vector $\hat{p} \in \mathbb{R}^N$.
The transformation that governs this dynamic pricing is a very simple surge-based pricing (a special case of our later defined policy $\pi$):
\begin{equation}
\hat{p}_i = \begin{cases}
@@ -150,22 +164,31 @@ p_{0,i} & \text{otherwise}
where $p_0 \in \mathbb{R}^N$ is the base price vector (which is seeded into our database distinctly for each mode of the commerce platform), $\theta_{\text{high}}, \theta_{\text{low}} \in \mathbb{R}$ are demand thresholds defining surge and discount regions, and $\lambda_{\text{surge}}, \lambda_{\text{disc}} \in \mathbb{R}^+$ are multiplicative factors with typical values $\lambda_{\text{surge}} = 1.2$ and $\lambda_{\text{disc}} = 0.9$. This piecewise function enables rapid price adjustment in response to observed demand without requiring complex elasticity estimation or historical calibration, allowing us to expose actors within our experiments to a system with a dynamic component of pricing.
We will for our offilne experimental intents generalize a master function for encompasing distinct demand estimation and pricing strategies.
\begin{align}
V(\cdot) = \max_{p_t} \min_{Q \in \mathcal{U}(\hat{d})}{\mathbb{E}_{d\sim Q} [p_t \times d(p_t, x_t ; \theta) + \psi V_{t+1}(\cdot)]}
\end{align}
We follow differnet substitutouns which will server as hyperparameters later on.
% For our offline experimental setting, we generalize a master value function that can encompass different demand estimation and pricing strategies.
%
% \begin{align}
% V(\cdot) = \max_{p_t} \min_{Q \in \mathcal{U}(\hat{d})}{\mathbb{E}_{d\sim Q} [p_t \times d(p_t, x_t ; \theta) + \psi V_{t+1}(\cdot)]}
% \end{align}
%
% We evaluate different substitutions of this objective, which later serve as hyperparameters in the simulator.
\subsection{Experimental Design}
The experimentation begins with the design of goals, with careful consideration to assure a uniform spanning across different variables within each product-architecture of either the hotel or airline platforms. Our crafted collection of goals (jobs to be done) is then tracked in a postgress database with one table to track goals and another table to track different experiment runs, and their associated goals in a experiment-goal one-to-one relationship.
We start from a practical constraint: we do not have access to proprietary production data. Because of that, we design our own fictional platform that still represents how commercial platforms work in the real world. The design comes from a survey of hotel and airline websites, where we extracted common interface components and used them as a high-level template for dynamic pricing environments.
The purpose of this effort to gather data on interactions, is the first half of our research. With this collected data on behavioral characteristics, enhanced by our feature augmentation, we can create distribution separation into two bins $y \in \{A,H\}$ with a certain probability $p$ dependent on the session-specific features. To address the second loop of our system, we use this gained capability of discrimination to enhance the learner design involved in our surrogate dynamic pricing task which simulates an independent dynamic pricing scenario under which we can train a more controlled policy with the ability to account for true demand signals under conditions of contamination from non-human actors.
The interface is organized as a product catalog where each product belongs to a time-bounded price vector (for example, a daily pricing period). During each period we collect interaction data by instrumenting UI components and predefined action templates that are still customizable. This gives us control without losing realism.
Since users act with motivations, we define a pool of tasks (jobs to be done) and assign tasks randomly to participants.
The task pool is stored as a structured table with fields \texttt{id}, \texttt{created\_at}, \texttt{task\_name}, \texttt{task\_description}, and \texttt{task\_def\_of\_done}. We formulate the tasks as compact jobs-to-be-done rather than as strict click scripts, because the target is to elicit realistic browsing and comparison behavior which can capture nuance of different people. In hotel mode the assigned tasks include \textit{Cheapest Room}, \textit{Cheapest Room w/ View}, \textit{MultiStep Cheapest Room}, \textit{The Digital Nomad (Executive)}, and \textit{The 3-Way Tradeoff (Desk + Quiet + Flexible)}. These prompts deliberately require critical thought in search, inspection of room details, comparison of amenities or images, return visits to the listing page, and a final booking decision which create a degree of cognitive load. In airline mode we use \textit{Last-Minute One-Way Flight}, where the actor must urgently travel to LAX from either SEA or JFK within the next 1--3 days, inspect at least a small set of candidate itineraries, and then book a reasonable earliest departure.
A representative task is to find the cheapest feasible catalog item under explicit constraints while removing strict financial limits so we avoid trivial optimization behavior. Participants are also randomly assigned to one experimental platform mode (hotel or airline). Once assigned, they are dropped into the experiment with an actor ID. Under each experiment ID, we can observe multiple sessions across time and gather long interaction traces for the same actor.
Our approach can be well summarized by a three-stage division, first we intend to observe and \textit{vectorize} the behavioral interaction data from our experiments, we then develop the separability which helps us deepen the semantic understanding of the behavioral patterns. Finally we use our newly gained learner to leverage a defensive mechanism within the simulation stage of a controlled dynamic pricing loop.
The human data collection involved 18 participants, all of whom provided explicit informed consent prior to their session. Participants had an average age of 21 years and were recruited from a university population. Alongside the 18 human sessions we ran 18 agent sessions of equivalent task scope, giving a balanced dataset of 36 labeled trajectories. Each participant was assigned a single platform mode and a single task drawn from the pool, and completed the session independently without guidance on navigation or pricing strategy.
To evaluate quality and realism of the setup, we store both structured event logs and full interaction transcripts. This lets us combine quantitative analysis with transcript-level qualitative findings. The result is an isolated system where we can control the interaction process while preserving realistic behavior.
Operationally, goals and experiment runs are tracked in PostgreSQL (goal table, run table, and assignment mapping). This data-acquisition phase is the first half of the methodology and is intentionally a disconnected component that feeds the later contributions. The second half uses collected behavioral traces to separate classes $\theta \in \{A,H\}$ with session-conditioned probability estimates, then injects those estimates into the pricing learner.
Our process follows three stages: (1) observe and \textit{vectorize} behavioral interactions, (2) learn separability to characterize human versus agent patterns, and (3) use the learned signal to train a defensive policy in a controlled dynamic-pricing simulator.
\begin{figure}[ht]
\resizebox{\columnwidth}{!}{%
@@ -174,23 +197,145 @@ Our approach can be well summarized by a three-stage division, first we intend t
\caption{Overview of the Dynamic Pricing Tasks.}
\end{figure}
Our web platform (developed in similar spirit to RecSim \parencite{ie_recsim_2019}) gives us a controlled environment where tasks are assigned to human and agentic actors and then executed. Each actor receives a browser-level experiment identifier that may persist across multiple session IDs. We then group by experiment and extract session trajectories using the schema below.
To speak to realism, user interviews reported that the platform architecture mirrored standard booking interfaces and reduced the cognitive load required to learn the system. One participant described the flow as ``intuitive'' and close to a ``normal'' transaction, suggesting observed behavior was primarily driven by pricing treatment rather than interface novelty.
The dynamic pricing mechanism elicited immediate behavioral adjustments. Participants were sensitive to price volatility: sudden boosts triggered urgency and faster booking attempts, while large listing-to-final discrepancies triggered deeper comparison behavior. This is comforting because the controlled setup still produces commercially relevant interaction data.
\subsubsection{Design of Training Factorial Study}
The simulator has multiple configurable factors. We design a multi-factor study across five axes derived from the sweep configurations: (1) RL algorithm (\texttt{ppo}, \texttt{a2c}, \texttt{dqn}, \texttt{qtable}; 4 levels), (2) contamination ratio $\alpha$ sampled from $[0.1, 0.6]$ at four representative levels, (3) robustness radius $\epsilon_\alpha \in \{0.0, 0.15, 0.3\}$ (3 levels), (4) COI penalty weight $\lambda_\text{coi}$ at two reference levels, and (5) pricing action granularity (two discretization settings for \texttt{action\_levels}); giving a grid of $4\times4\times3\times2\times2 = 192$ configurations. Statistical power for the behavioral comparisons is determined by a two-sample test over per-session KL divergence scores; a formal power analysis with minimum detectable effect size at $n=18+18$ is reported in the results.
% Power analysis plan: apply a two-sample Mann-Whitney U (or permutation test) on per-session (delta_H - delta_A) divergence scores comparing the human and agent groups. Compute minimum detectable effect size at alpha=0.05, power=0.8, given n=18 per group. Bootstrap confidence intervals on mean KL are a cleaner complement given the non-normality of divergence distributions.
While this scale is generally expensive for reinforcement learning, we execute it on a large TPU cluster to make the sweep tractable.
Our training budget is provisioned through TPU Research Cloud and spans 384 chips across TPU v4, v5e, and v6e generations, with a spot-heavy allocation plus an on-demand reserve. At peak BF16 throughput this corresponds to approximately 160\,PFLOPS of aggregate compute (derivation in Appendix~\ref{app:compute_budget}), which makes repeated seeds, ablations, and sensitivity sweeps feasible within practical wall-clock limits. We allocate v6e capacity to the highest-intensity policy training jobs, use v5e for wider hyperparameter exploration where throughput-per-dollar is favorable, and reserve on-demand v4 capacity for runs that should not be interrupted.
\begin{table}[ht]
\centering
\caption{Compact comparison of TPU generations used in the training stack.}
\label{tab:tpu_specs}
\begin{tabular}{@{}llll@{}}
\toprule
\textbf{Feature} & \textbf{TPU v4} & \textbf{TPU v5e} & \textbf{TPU v6e (Trillium)} \\
\midrule
Peak BF16 per chip (TFLOPS) & 275 & 197 & 918 \\
HBM capacity per chip (GB) & 32 & 16 & 32 \\
HBM bandwidth per chip (GB/s) & 1200 & 819 & 1600 \\
TensorCores per chip & 2 & 1 & 1 \\
Interconnect topology & 3D mesh/torus & 2D torus & 2D torus \\
Max pod size (chips) & 4096 & 256 & 256 \\
\bottomrule
\end{tabular}
\end{table}
\begin{table}[ht]
\centering
\caption{TPU allocation used for the factorial study.}
\label{tab:tpu_allocation}
\begin{tabular}{@{}llll@{}}
\toprule
\textbf{TPU Type} & \textbf{Total Chips} & \textbf{Zone(s)} & \textbf{Provisioning} \\
\midrule
v6e & 128 (64 + 64) & europe-west4-a, us-east1-d & Spot \\
v5e & 128 (64 + 64) & us-central1-a, europe-west4-b & Spot \\
v4 & 64 (32 + 32) & us-central2-b & 32 Spot + 32 On-demand \\
\bottomrule
\end{tabular}
\end{table}
For connections from Madrid, we prioritize the europe-west4 allocation for latency-sensitive runs with the benefit of having the most grouped chips within a single region. This regional grouping is important for the deployment of our Kubernetes cluster which cannot span multiple regions. All sweep metadata, model checkpoints, and reward traces are logged in Weights \& Biases. Hardware specifications are from the official Google Cloud TPU documentation \parencite{noauthor_tpu_2026,noauthor_tpu_2025-1,noauthor_tpu_2025}.
Design of training processes: we build docker image with the fact in mind of different caching over layers in order to most speed up docker re-building and such we place the most volatile steps towards the end of the image building. What is means in practice is that any dependency installations are isolated so edits to source code do no trigger rebuilds. Only if we update our entry point of training a sweep, Docker will also rebuild the source-code copy stage.
Due to the preemptive nature of the current demand of TPU chips we sttle for running our on demeaned as the primary source of compute. The on demand TPU pod of 32 chips spread across 4 virtual hosts creates a relatively unique parallelization setup. Despite our desire to use a traditional approach of clustering and perhaps deploying SLURM jobs of our sweep agent, the lack of predictability in provisioning each instance of a compute resource makes this an high friction layer we do not want to add.
\subsubsection{Interaction Schema}
We extend the basic event tuple $e_{s,k}$ to capture the full observational signal available to the platform. An interaction event is defined as the extended tuple:
\begin{equation}
e_{s,k} = \left( a_{s,k}, \, i_{s,k}, \, t_{s,k}, \, \mu_{s,k}, \, \delta_{s,k} \right)
\end{equation}
where $\mu_{s,k} \in \mathcal{M}$ is a metadata record containing action-specific context (e.g., price observed, filter parameters, element text), and $\delta_{s,k} \in \mathbb{R}_+$ is the dwell time in milliseconds for attention-based actions.
A session $s$ is itself a structured record:
\begin{equation}
s = \left( \text{sid}, \, \text{eid}, \, t_0, \, \phi, \, \mathcal{U}, \, \tau_s \right)
\end{equation}
where $\text{sid}$ is a unique session identifier (UUID), $\text{eid}$ optionally links to an experiment, $t_0$ is the session start timestamp, $\phi \in \{\texttt{hotel}, \texttt{airline}\}$ denotes the platform mode, $\mathcal{U}$ is the user-agent string, and $\tau_s$ is the trajectory of events.
The action space $\mathcal{A}$ is partitioned into four semantic categories based on the behavioral signal each action conveys:
\begin{table}[ht]
\centering
\caption{Action space partition $\mathcal{A} = \mathcal{A}_{\text{nav}} \cup \mathcal{A}_{\text{cart}} \cup \mathcal{A}_{\text{filter}} \cup \mathcal{A}_{\text{dwell}}$ with signal interpretation.}
\label{tab:action_space}
\begin{tabular}{@{}llll@{}}
\toprule
\textbf{Category} & \textbf{Actions} & \textbf{Signal} & $\boldsymbol{\omega}$ \\
\midrule
$\mathcal{A}_{\text{cart}}$ & \texttt{add\_item}, \texttt{remove}, \texttt{checkout}, \texttt{purchase} & Purchase intent & High \\
$\mathcal{A}_{\text{dwell}}$ & \texttt{hover\_title}, \texttt{hover\_paragraph}, \texttt{hover\_link} & Sustained attention & Medium \\
$\mathcal{A}_{\text{nav}}$ & \texttt{page\_view}, \texttt{view\_item}, \texttt{learn\_more} & Discovery & Low \\
$\mathcal{A}_{\text{filter}}$ & \texttt{search}, \texttt{filter\_date}, \texttt{filter\_price}, \texttt{sort} & Preference refinement & Lowest \\
\bottomrule
\end{tabular}
\end{table}
This partition enables the weight function $\omega$ from Eq.~\ref{eq:qhat} to assign category-specific signal strengths, with $\omega(\mathcal{A}_{\text{cart}}) > \omega(\mathcal{A}_{\text{dwell}}) > \omega(\mathcal{A}_{\text{nav}}) > \omega(\mathcal{A}_{\text{filter}})$ reflecting decreasing commitment.
Its important to acknowledge that this creates a very blatant assumption in the weighting, we do motivate the scale of each weight by the per-category observed divergence between each behavioral profile.
In the simulator baseline this order is encoded with a compact fixed scale: cart $=4.0$, dwell $=2.0$, nav $=1.0$, filter $=0.5$. Unknown actions are mapped by prefix heuristics to the nearest category.
The metadata record $\mu$ varies by action type. For product views, $\mu$ contains the observed price $p_{\text{obs}}$ and product attributes. For dwell events, $\mu$ includes the element text and accumulated hover duration. This heterogeneous structure is captured via a schema-on-read approach in our Kafka ingestion pipeline, where events are validated against type-specific schemas before storage.
In addition to behavioral events, the platform logs price observations to a separate Kafka topic. Each price query generates a record $(i, p, \text{sid}, \phi, t)$ associating the product, displayed price, requesting session, platform mode, and timestamp. This dual-stream architecture enables joint analysis of price exposure and behavioral response.
Study methodology and approach. Data acquisition strategy. Defined objectives and success criteria. Observable metrics and KPIs.
\subsection{Generative Contamination and Separability}
To develop a robust pricing agent, we require a simulation environment capable of generating realistic, contaminated interaction data. We achieve this by learning from our Phantom platform data using a two-stage approach.
To train a robust pricing learner, we need a simulator that can generate realistic interaction data under controlled contamination. We build this from Phantom data using a two-stage approach.
\subsubsection{Ground-Truth Separability}
Because sessions are collected under controlled experimental conditions where each actor is assigned a known type at the start of the trial, labels $\theta_s \in \{H, A\}$ are available as ground truth rather than as the output of a heuristic classifier. We therefore estimate separate transition kernels directly from each labeled partition $\mathcal{D}_H$ and $\mathcal{D}_A$, treating the resulting $\hat{\mathcal{T}}_H$ and $\hat{\mathcal{T}}_A$ as the ground-truth behavioral profiles for each class. We then ask a direct methodological question: are the kernels separable enough to justify downstream pricing control that depends on that separability?
\subsubsection{GOFAI-Based Separability}
We employ Good Old-Fashioned AI (GOFAI) heuristics to generate initial weak labels for separability. We define a set of rule-based predicates $\phi_j: \tau \to \{0, 1\}$ to partition the dataset $\mathcal{D}$ into high-confidence sets $\mathcal{D}_H$ and $\mathcal{D}_A$. We construct distinct MDPs per each behavioral profile of humans and agents and from those we establish $D_{KL}$. From initial findings we compute a KL divergence of $\approx 2.0236$ across transition probabilities between states which can be seen in \ref{fig:human_mdp_viz} and \ref{fig:agent_mdp_viz}.
To answer this, we compute per-session KL divergence scores against both class-level centroids. For each session $s$ in either partition, we fit a session-level event transition kernel $\hat{\mathcal{T}}_s$ from that session's trajectory alone, then compute its average KL divergence to the human centroid ($\Delta_{H,s}$) and to the agent centroid ($\Delta_{A,s}$). The per-session separability score is the gap $\Delta_{H,s} - \Delta_{A,s}$: a negative value indicates proximity to human behavior, a positive value indicates proximity to agent behavior.
The normality assumption cannot be made for KL divergence distributions, which are right-skewed and bounded below by zero, so we do not use a Student's $t$-test. Instead we apply a Mann-Whitney $U$ test \parencite{mann_test_1947} on the per-session gap scores between the two groups. The Mann-Whitney test is a rank-based nonparametric test that compares the stochastic ordering of two independent samples without distributional assumptions, making it appropriate for small samples drawn from skewed populations. We report $U$, the exact two-sided $p$-value, and group-level descriptive statistics for the gap scores.
\begin{definition}[Kullback-Leibler Divergence for Transition Distributions]
Let $P_e$ and $Q_e$ be categorical distributions over destination states following event $e$, derived from human and agent trajectories respectively. The KL divergence between these distributions is:
\begin{equation}
D_{\mathrm{KL}}(P_e \parallel Q_e) = \sum_{k \in \mathcal{S}_e} P_e(k) \log \frac{P_e(k)}{Q_e(k)}
\end{equation}
where $\mathcal{S}_e$ denotes the set of destination events that follow $e$ in the human trajectories.
\end{definition}
To obtain this statistic, we aggregate transitions by triggering event $e$ and treat normalized outgoing probabilities as categorical distributions $P_e$ (human) and $Q_e$ (agent). We intersect shared event labels, then accumulate log-ratio contributions over shared destinations. Large contributions, including near-zero $Q_e(k)$ cases, identify transitions where one actor class is difficult to mimic.
With these divergence features we train a contrastive model to estimate a weak agent probability $f(\tau)\in[0,1]$, which we later use as a weighting and control signal.
\subsubsection{Transition Probability Estimation}
\label{sec:tpe}
For both subsets, we model session dynamics as an MDP and estimate transition kernel $\mathcal{T}$. For each actor type we estimate global kernels $\hat{\mathcal{T}}_A$ and $\hat{\mathcal{T}}_H$, then cluster into behavioral sub-kernels $\hat{\mathcal{T}}_y^i$ to avoid collapsing all behavior into one average profile. Transition probabilities are estimated by maximum likelihood:
\begin{equation}
\hat{P}(s' \mid s) = \frac{N(s, s')}{\sum_{k \in \mathcal{S}} N(s, k)}
\end{equation}
where $N(s, s')$ is the observed transition count. This allows us to construct a \textit{Contamination Generator} $\mathcal{G}(\alpha)$. Given a clean trajectory dataset, $\mathcal{G}$ injects synthetic agent trajectories sampled from $\hat{\mathcal{T}}_A$ until the effective mixing ratio reaches $\alpha$. The properties of an MDP such as ... should be preserved by the operation described below.
To scale this to catalog-level pricing, we expand the base event transition matrix from $T\times T$ into product-specific transitions using the current demand condition. In practice, we normalize the demand vector across products and use it to weight how much transition mass each product pair receives. Concretely, each cell of the base matrix becomes an $N\times N$ block (for $N$ products), so the transition matrix grows from $T\times T$ to $(T\cdot N)\times(T\cdot N)$. Finally, we add $C$ generic states (homepage, login, checkout terminal states), which gives the full kernel size $(T\cdot N + C)\times(T\cdot N + C)$.
% The validity of this demand-weighted block expansion is still subject to formal proof: it needs to be shown that the resulting matrix retains row-stochasticity (rows summing to 1) and that the weighting by the demand vector preserves the Markov property for the expanded state space. In the engine source this is the target of ongoing validation before the expansion is relied on for behavioral generation at scale.
\begin{figure}[ht]
\centering
\includegraphics[width=0.8\textwidth]{chapters/mdp_human.pdf}
\caption{Markov Decision Process visualization illustrating the behavioral transition dynamics for human actions.}
\caption{Markov Decision Process visualization illustrating the behavioral transition dynamics for \textbf{human} actions.}
\label{fig:human_mdp_viz}
\end{figure}
@@ -201,36 +346,108 @@ We employ Good Old-Fashioned AI (GOFAI) heuristics to generate initial weak labe
\label{fig:agent_mdp_viz}
\end{figure}
\subsubsection{Transition Probability Estimation}
For both subsets, we model the session dynamics as a Markov Decision Process (MDP) and estimate the transition kernel $\mathcal{T}$. The probability of transitioning to state $s'$ given state $s$ is estimated via maximum likelihood:
\begin{equation}
\hat{P}(s' \mid s) = \frac{N(s, s')}{\sum_{k \in \mathcal{S}} N(s, k)}
\end{equation}
where $N(s, s')$ is the count of observed transitions. This allows us to construct a \textit{Contamination Generator} $\mathcal{G}(\alpha)$. Given a clean trajectory dataset, $\mathcal{G}$ injects synthetic agent trajectories sampled from the learned transition matrix $\hat{P}_A$ until the effective mixing ratio reaches $\alpha$.
\subsection{Distributionally Robust Reinforcement Learning (DR-RL)}
We formulate the pricing problem as a Stackelberg Game where the Platform (Leader) sets prices $p_t$ and the Aggregate Demand (Follower) responds. However, the exact mixing parameter $\alpha$ and the demand distribution shift are non-stationary and unknown in online settings. Relying on a simple error term $\epsilon$ is insufficient. Instead, we adopt a Distributionally Robust Optimization (DRO) objective.
We formulate pricing as a Stackelberg game: the platform (leader) sets prices $p_t$, and the population (follower) responds through trajectories and demand. A useful intuition is that the platform behaves like a distorted mirror at a 45-degree angle: what it mirrors is population demand into an estimated demand proxy, and that proxy drives revenue.
Because contamination level $\alpha$ and demand shift are non-stationary online, a simple error term is not enough. We therefore use a Distributionally Robust Optimization objective. Let $\tau'$ be a newly observed trajectory generated by an unknown actor profile (sampled from the behavioral models in Section~\ref{sec:tpe}). We need a demand mapping conditioned on price and trajectory, $\hat{Q}(p,\tau')$. For each $\tau'$, we compute $\hat{\mathcal{T}}'$ and compare it with controlled baselines $\bar{\mathcal{T}}_H$ and $\bar{\mathcal{T}}_A$:
\begin{align}
\label{eq:delta_H}
\Delta_H &= D_{KL}(\hat{\mathcal{T}}^\prime \parallel \bar{\mathcal{T}}_H) \\
\label{eq:delta_A}
\Delta_A &= D_{KL}(\hat{\mathcal{T}}^\prime \parallel \bar{\mathcal{T}}_A)
\end{align}
This yields two centroid-like heuristics that act as a session-level agent score in the engine. On a per-customer or use-case basis a similar study should be done in order to obtain ground truth behavior models for humans and agents and their specific interaction with a given products website.
In implementation, we maintain an alternating game-history stack (our \textit{Limbo} stack) and execute it explicitly every epoch with exactly two transitions: first the platform publishes a price vector (leader move), then the market responds with trajectory-derived demand (follower move).
% Mention discretized action space and the clipping and over shotting in continuous action spaces
% Also talk about catastrophic economics, we add termination on bankrupcy or zero demand so market collaps
\subsubsection{Ambiguity Set Construction}
We define an ambiguity set $\mathcal{U}_p(\hat{P}_N)$ centered around our empirical reference distribution $\hat{P}_N$ (derived from the generator $\mathcal{G}$). We utilize the Wasserstein distance metric to define the set of plausible demand distributions the agent might face:
We define an ambiguity set $\mathcal{U}_\epsilon(\hat{P}_N)$ centered around our empirical reference distribution $\hat{P}_N$ (derived from the generator $\mathcal{G}$). We utilize the Wasserstein distance metric to define the set of plausible demand distributions the agent might face:
\begin{equation}
\mathcal{U}_\epsilon(\hat{P}_N) = \left\{ Q \in \mathcal{P}(\Xi) : W_p(Q, \hat{P}_N) \le \epsilon \right\}
\end{equation}
This set captures all distributions that are statistically close to our observed training data but allows for adversarial shifts (e.g., sudden bot spikes).
This set captures all distributions that are statistically close to our observed training data but allows for adversarial shifts.
For the current engine baseline, we use a compact inner-robust approximation by applying ambiguity over contamination in a local interval around nominal contamination $\alpha_0$:
\begin{equation}
\mathcal{A}_{\epsilon_\alpha}(\alpha_0)=\left\{\alpha\in[0,1]:\lvert\alpha-\alpha_0\rvert\le\epsilon_\alpha\right\}
\end{equation}
and we evaluate a small fixed grid in $\mathcal{A}_{\epsilon_\alpha}(\alpha_0)$ per step, selecting the worst-case candidate for the learner.
% A proper Wasserstein ball implementation over the full demand distribution (rather than a scalar alpha interval) would use the POT library (Python Optimal Transport): compute W_2 between the empirical reference P_hat and each candidate Q using ot.emd2() or ot.sliced_wasserstein_distance() for scalability, then accept only candidates within epsilon. In practice the inner minimization becomes: candidates = [G(alpha) for alpha in linspace]; dists = [ot.emd2(p_hat, q, M) for q in candidates]; worst = candidates[argmin(reward[dists <= epsilon])]. The current grid-on-alpha approximation is a computationally cheap substitute; moving to a true Wasserstein ball would tighten the worst-case guarantee but requires specifying the ground metric M over the demand space.
\subsubsection{Environment Setup for Dynamic Pricing}
The complete pricing-demand-trajectory loop is illustrated in Figure~\ref{fig:oracle_flow}. The Oracle maps historical price and demand state to a new price vector, which is exposed to a distribution of demand curves. Each product generates trajectories weighted by behavioral kernels $\tau_\theta$, producing a full transition matrix $\tau'$ over sessions. Sampled trajectories $\{\tau_k\}$ are aggregated through the demand proxy function $Q(\cdot)$ to yield the next demand vector, which feeds back into the Oracle.
\begin{figure}[ht]
\centering
\[
\text{Oracle}(\vec{p}_{t-1},\vec{\hat{q}})\to
\begin{pmatrix}
p_0\\
p_1\\
\cdots\\
p_N
\end{pmatrix}
\underrightarrow{d_i \sim \mathcal{N}_{\vec{p}}}
\begin{pmatrix}d_0\\ d_1\\ \cdots \\ d_N\end{pmatrix}
\underrightarrow{\vec{d}\times \tau_\theta \to \tau^\prime}
\begin{bmatrix}
0.01 & 0.02 & \cdots & 0.3 \\
0.41 & 0.24 & \cdots & 0.0 \\
\cdots & \cdots & \cdots & \cdots \\
0.51 & 0.09 & \cdots & 0.1 \\
\end{bmatrix}
\underrightarrow{\tau_k \sim \tau^\prime}
\{\tau_k\}_{k=0}^K \to \hat{Q}(\tau_k)
\to \begin{pmatrix}
\hat{q}_0 \\
\hat{q}_1 \\
\cdots \\
\hat{q}_N \\
\end{pmatrix}
\to \text{Oracle}(\cdot)
\]
\caption{Oracle-based pricing loop: historical price and demand state map to a new price vector; each product samples demand curves from $\mathcal{N}_{\vec{p}}$; trajectories are generated by mixing demand with behavioral kernels $\tau_\theta$ into transition matrix $\tau'$; sampled trajectories $\{\tau_k\}$ aggregate through proxy $Q(\cdot)$ to yield updated demand $\vec{\hat{q}}$, closing the feedback loop.}
\label{fig:oracle_flow}
\end{figure}
\subsubsection{The Min-Max Objective}
The robust policy $\pi^*$ is obtained by solving the maximin problem:
\begin{equation}
\pi^* = \arg \max_{\pi} \min_{Q \in \mathcal{U}_\epsilon} \mathbb{E}_{d \sim Q} \left[ R(p, d) - \lambda \cdot \text{COI}(p) \right]
\label{eq:robust_policy}
\pi^* = \arg \max_{\pi} \min_{Q \in \mathcal{U}_\epsilon} \mathbb{E}_{d \sim Q} \left[ R(p, d) - \lambda \cdot \text{COI}_{\text{leak}}(p,\tau') \right]
\end{equation}
where $R(p, d)$ is the revenue function and $\lambda$ weighs the penalty for information leakage (COI).
where $R(p, d)$ is the revenue function and $\lambda$ weighs the information-leakage penalty.
In practice, we parameterize this with a session-level leakage term:
\begin{equation}
\text{COI}_{\text{leak}}(p,\tau') = f(\tau')\cdot \text{InfoValue}(p,\tau')
\end{equation}
where $f(\tau')$ is the weak agent probability and $\text{InfoValue}$ is implemented either as a constant query-tax surrogate or as a revelation surrogate $-\log\pi(p\mid\tau')$.
For the baseline engine reported here, we intentionally use the constant query-tax surrogate to keep the mechanism minimal:
\begin{equation}
r_t = R(p_t,\tilde q_t) - \lambda\,f(\tau_t')\,c_{\text{info}}
\end{equation}
with fixed $c_{\text{info}}>0$.
Another possible extension is to adapt the ambiguity radius online, e.g., $\epsilon(\Delta_H)$, so the Wasserstein ball changes with live divergence. We keep this as future work and retain a fixed-radius setup because Wasserstein ambiguity already handles heavy-tail and ``black swan'' behavior without absolute continuity assumptions \parencite{kuhn_wasserstein_2024}.
\subsubsection{Actor Implementation}
In our simulation, the "Follower" is implemented as a set of Actors. Each Actor is initialized with a type $\theta$ which samples a specific demand curve $d(p; \theta)$ from the latent distribution. This formalization ensures that our DR-RL agent does not overfit to a single deterministic demand function but learns a policy robust to the distributional uncertainty defined by $\mathcal{U}_\epsilon$.
In our simulation, the ``follower'' is implemented as a set of Actors. Each Actor is initialized with a type $\theta$ which samples a specific demand curve $d(p; \theta)$ from the latent distribution. This formalization ensures that our DR-RL agent does not overfit to a single deterministic demand function but learns a policy robust to the distributional uncertainty defined by $\mathcal{U}_\epsilon$.
Practical implementation of browser agents is a strongly evolving field with near-weekly releases of SOTA architectures. In this thesis implementation we abstract that layer into trajectory generators learned from observed human/agent transition kernels.
As part of our reward engineering we think about the UX factor ($UX \in [0,1]$) whic his our proxy for user experience degradation, this is computed as a mixture of contribution from the separability model metric of $\frac{1}{\text{Specificity}}$.
As part of reward engineering, we keep a UX factor ($UX\in[0,1]$) as an auxiliary evaluation axis. In the current baseline it is not injected into the core reward; it is tracked separately to compare policy trade-offs.
\begin{figure}[ht]
\centering
@@ -240,12 +457,40 @@ As part of our reward engineering we think about the UX factor ($UX \in [0,1]$)
\caption{Introducing the UX index allows us to better distinguish the kind of impact different methods have and allows us to compare them on this Pareto-like scale.}
\end{figure}
We also need to think about a policy like taxation to the agents Strategy-Proof Mechanism Design, specifically the Vickrey-Clarke-Groves (VCG) payment rule. We link and prove that this would create an incentive for the dominant strategy to become truth-telling.
We also consider taxation-like overlays for agent traffic under strategy-proof mechanism design (e.g., Vickrey-Clarke-Groves style rules). This remains an extension path and is not part of the main implementation in this thesis.
\section{Heuristics as part of neuro-inspired steering systems}
\subsubsection{Pricing Mechanism Summary}
Steve Burns, superior culliculus (face heuristics) we create this sort of part of the 'brain' + amortized inference.
We now present the complete pricing mechanism that integrates the behavioral separability, contamination estimation, and robust optimization components developed in the preceding sections. Algorithm~\ref{alg:phantom_loop_clean} formalizes the defensive pricing loop as a Stackelberg game where the platform (leader) sets prices and the aggregate demand (follower) responds through observed session trajectories.
We could say that a DQN for example is the learnin subsystem and then within our reward mechanism or some other computational method we introduce a steering subsystem which acts as the proposed ``pricing heuristic'' against the given non human transaction data.
\begin{algorithm}[t]
\caption{PHANTOM defensive pricing loop}
\label{alg:phantom_loop_clean}
\DontPrintSemicolon
\SetKwInput{Input}{Input}
\SetKwInput{Output}{Output}
\section{Market construction}
\Input{catalog size \(N\); action scale grid \(\mathcal{S}_{act}\); nominal contamination \(\alpha_0\); ambiguity radius \(\epsilon_\alpha\); candidate count \(K\); horizon \(T\); sessions per step \(M\); behavior kernels \(\bar T_H,\bar T_A\); event weights \(\omega\); COI penalty \(\lambda\)}
\Output{trajectory \(\{(p_t,\hat Q_t,\alpha_t^*)\}_{t=0}^{T-1}\)}
\For{\(t \leftarrow 0\) \KwTo \(T-1\)}{
observe \(o_t=[\hat Q_{t-1}, p_{t-1}]\)\;
choose discrete action \(a_t \in \{1,\dots,|\mathcal{S}_{act}|\}\) from policy \(\pi\)\;
set \(p_t \leftarrow \mathrm{clip}(p_{t-1} \cdot \mathcal{S}_{act}[a_t])\)\;
define local ambiguity interval \(\mathcal{A}_{\epsilon_\alpha}(\alpha_0)=\{\alpha:\lvert\alpha-\alpha_0\rvert\le\epsilon_\alpha\}\)\;
\For{\(k \leftarrow 1\) \KwTo \(K\)}{
set \(\alpha_k \in \mathcal{A}_{\epsilon_\alpha}(\alpha_0)\) from a uniform grid\;
sample \(M\) sessions from mixture \((1-\alpha_k)\bar T_H + \alpha_k \bar T_A\)\;
compute demand proxy \(\hat Q_t^{(k)} = \sum_{m=1}^{M}\sum_j \omega(a_{m,j})\,\mathbf{1}[i_{m,j}=i]\)\;
compute \((\Delta_H^{(k)},\Delta_A^{(k)})\) and session score \(f_t^{(k)}\) from KL divergence\;
compute candidate reward \(r_t^{(k)} = R(p_t,\hat Q_t^{(k)}) - \lambda\,f_t^{(k)}\,c_{info}\)\;
}
choose \(k^* \leftarrow \arg\min_k r_t^{(k)}\), set \(\alpha_t^* \leftarrow \alpha_{k^*}\)\;
set \(\hat Q_t \leftarrow \hat Q_t^{(k^*)}\), \(r_t \leftarrow r_t^{(k^*)}\)\;
}
\end{algorithm}
The algorithm operates in discrete epochs indexed by $t$. At each epoch, the platform applies one discrete multiplicative price action, the environment samples a batch of sessions, and demand is recomputed from weighted events. Robustness is implemented as an inner minimization over a small local grid of contamination candidates around nominal $\alpha_0$, matching the current engine implementation. The history buffer $\mathcal{L}$ (``Limbo'' in our implementation) enforces the alternating Stackelberg structure by preserving the temporal sequence of price publications and demand observations.
%The defensive price update in Line 24 implements contamination-aware margin shrinkage: as estimated contamination $\hat{\alpha}_t$ rises, the margin $(p^{\mathrm{ref}} - c)$ is reduced by factor $\kappa\in[0,1]$, with projection $\Pi_{\mathcal{P}}$ ensuring feasibility. In subsequent experiments this heuristic rule is replaced by DR-RL policy $\pi^*$ from Eq.~\ref{eq:robust_policy}.

View File

@@ -1,16 +1,67 @@
\section{Results}
\begin{figure}[ht]
\centering
\input{chapters/figures/supra.tex}
\caption{Evolution of price distributions over experiment steps. The heatmap illustrates the density of price offerings. This is an early baseline simulation which demonstrates supra-competitive price-setting in deep learning agents such as SAC as can be clearly seen by the high density at the highest available price.}
\label{fig:supra_heatmap}
\end{figure}
\subsection{Behavioral Analysis}
Include markov chains of transition matrices, compare distributions (look at Divergence metrics)
Separability between human and agent sessions is evaluated by computing per-session divergence gap scores $\Delta_{H,s} - \Delta_{A,s}$ and comparing the two groups with a Mann-Whitney $U$ test. Table~\ref{tab:divergence_significance} reports the group-level descriptive statistics for the gap scores and the test result.
\begin{table}[ht]
\centering
\caption{Per-session divergence gap ($\Delta_H - \Delta_A$) by actor class with Mann-Whitney $U$ test.}
\label{tab:divergence_significance}
\begin{tabular}{lccc}
\toprule
Group & $n$ & Mean gap & Std \\
\midrule
Human sessions & 11 & $-3.3522$ & $2.6748$ \\
Agent sessions & 6 & $+1.6482$ & $2.8349$ \\
\midrule
\multicolumn{4}{l}{Mann-Whitney $U = 2.0$, $p = 0.0006$ (two-sided)} \\
\bottomrule
\end{tabular}
\end{table}
The sign structure is consistent with the theoretical expectation: human sessions produce negative gap scores (closer to the human centroid, far from the agent centroid) while agent sessions produce positive gap scores (closer to the agent centroid). The two-sided $p$-value of $0.0006$ indicates near-complete rank separation between the groups at $n_H=11$, $n_A=6$, providing strong evidence that the transition kernels are separable enough to justify their use as a control signal in downstream pricing.
\subsection{Experimental Outcomes}
Align with defined objectives, show results and statistical significance (or not).
To evaluate robustness contributions, we compare two policies on the same environment family: (i) robust pricing with COI-aware reward and adversarial contamination step, and (ii) non-robust baseline with revenue-only reward (\texttt{--no-robust}).
We report two preliminary stages before the full factorial interpretation. First, we executed a short calibration run at $\alpha=0.3$ (2 evaluation episodes, 3000 training timesteps per tier) across \texttt{qtable}, \texttt{ppo}, \texttt{a2c}, and \texttt{dqn}. In that first run, \texttt{ppo} produced the highest objective score and revenue (objective $=3.76\mathrm{e}5$, revenue $=4.15\mathrm{e}5$), while the remaining tiers stayed lower in this small-budget regime. The corresponding price traces show a monotone escalation for \texttt{ppo} (mean price from $8.61\mathrm{e}1$ to $1.49\mathrm{e}2$), whereas \texttt{qtable}, \texttt{a2c}, and \texttt{dqn} remained nearly flat over the episode horizon. This confirms that the simulation loop is able to express policy-dependent pricing dynamics rather than collapsing into a single trajectory shape.
Second, we launched an overnight paired benchmark over $\alpha \in \{0.00,0.15,0.30,0.45,0.60\}$ with 8 evaluation episodes and 8000 timesteps, comparing robust and non-robust settings at fixed seed/tier/contamination tuples. At the time of writing, two seeds (11 and 22) are complete and one additional seed is still running. We therefore frame the numbers below as an initial signal, not a final claim.
\begin{table}[ht]
\centering
\caption{Early overnight aggregate over completed seeds ($n=2$; seeds 11 and 22).}
\label{tab:pricing_benchmark}
\begin{tabular}{lcccc}
\toprule
Mode & Mean objective score & Mean revenue & Mean COI level & Mean margin \\
\midrule
Robust & $3.41\mathrm{e}5$ & $3.80\mathrm{e}5$ & $1.08\mathrm{e}2$ & 0.901 \\
Non-robust (\texttt{--no-robust}) & $3.91\mathrm{e}5$ & $4.18\mathrm{e}5$ & $1.11\mathrm{e}2$ & 0.906 \\
\bottomrule
\end{tabular}
\end{table}
At pair level (same seed, tier, and contamination), robust exceeds non-robust in $13/40$ configurations on objective score and in $16/40$ configurations on revenue. The current early evidence therefore suggests a conditional robustness effect: the defense is active and measurable, but not yet uniformly beneficial without further calibration.
\subsection{Interpretation and Insights}
Inference from given patterns and show key findings.
The Mann-Whitney result ($U=2.0$, $p<0.001$) confirms that per-session divergence gaps separate the two actor classes with near-zero overlap in rank ordering. This is the condition required for separability to act as a useful control signal in the pricing loop rather than just an auxiliary classifier score.
The first calibration and overnight runs additionally confirm three practical points aligned with the thesis mechanism. First, the control loop is reproducible end-to-end (training, evaluation, artifact generation) across algorithms and contamination levels. Second, policy class materially changes price trajectories and resulting COI/revenue profiles under identical environment settings. Third, objective improvements from robustness are regime-dependent in the current baseline, which is consistent with the thesis claim that contamination-aware pricing needs explicit calibration rather than a one-size-fits-all penalty.
We also note that maximizing revenue in isolation can favor aggressive high-price behavior; even in these early runs, the non-robust aggregate shows slightly higher mean COI and margin. For this reason, all subsequent reporting in this thesis is interpreted on a multi-metric basis (objective, revenue, COI, and stability), and not by revenue alone.
\subsection{Anomalies}
In our initial runs, we observed an instability pocket in one completed run (A2C, robust, seed 11, $\alpha=0.30$) with a large performance drop relative to neighboring configurations. We retain this run in the preliminary summary to avoid survivorship bias and treat it as evidence that robustness sensitivity analysis is necessary before final conclusions.

View File

@@ -1,18 +1,20 @@
\section{Discussion}
\subsection{Transition to Agentic Market Microstructure}
Our analysis of the interaction dynamics between the platform and non-human actors suggests that the current static pricing models are insufficient for an agent-mediated economy. If we assume a transition toward a direct revelation mechanism, where actors must reveal their true valuation of a good through bidding dynamics, we inevitably introduce significant stochasticity into the pricing system. Unlike traditional e-commerce where prices are relatively sticky, such a mechanism implies a high volatility characteristic of financial equity markets (without the fungability however).
However, ecommerce commodities differ fundamentally from financial securities: they possess a hard floor defined by unit economics and reservation prices. The market might react enthusiastically to an iPhone priced at \$1, such a transaction is not permissible. The platform must establish an initial valuation anchor ($P_{0}$) defined by the marginal cost plus a target margin, around which the market price is permitted to fluctuate. We propose the introduction of GenAI Agents as Institutional Market Makers.
This is also under the assumption of expected transactional capabilities being given to AI Agents.
However, ecommerce commodities differ fundamentally from financial securities: they possess a hard floor defined by unit economics and reservation prices. The market might react enthusiastically to an iPhone priced at \$1, such a transaction is not permissible. The platform must establish an initial valuation anchor ($P_{0}$) defined by the marginal cost plus a target margin, around which the market price is permitted to fluctuate. We float the introduction of GenAI Agents as Institutional Market Makers. As the arms race for greater autonomy of agnetic systems grows, the commercial viability of AI agents has the potential to disseminate into every-day users directly interacting with them rather than e-commerce platforms. This is also under the assumption of expected transactional capabilities being given to AI Agents.
\subsection{Risk Assessment and Limitations}
Acknowledge risks and constraints and data sizes.
This technology does not come without a more bitter side, ethical concerns do arise from the idea of deploying black-box like solutions to set prices based on a behavioral attributes. Approaches like universal behavioral profile modeling (UBPM) used in recommendation systems is very broadly utilized.
With a system like this there is potential for strong drift given the rapid advance of agentic systems and user preference. Our intent behind adding the UX term into the reward shaping process was to further address the risk of degraded user experience. Looking deeper at the underlying methodology, reinforcement learning does not come without it's complications such as reward hacking and often the lack of intepretability which is quite critical in systems that have a strong impact on the revenue of a company.
\subsection{Implications of Findings}

View File

@@ -1,8 +1,11 @@
\section{Conclusion}
For our troubles, we now conclude that...
\subsection{Summary of contributions}
Restate the thesis and key findings with validation of research objectives.
The authors contribution was not without the advice of many experienced experts in the field. We thank Marco Casalaina VP Products, Core AI and AI Futurist at Microsoft for the initial critical discussion on the topic of dynamic pricing systems and the spark which has lead to this work. Eugene Bykovets, PhD pointing out the parallels in blockchain systems and the complexity of anonymous interaction and understanding of intent. Importantly, the contributions of Alberto Martín Izquierdo, my academic advisor for the support over and for taking on the challenge of this ambitious work. Many breakthroughs were thanks to numerous discussions with my peers on the topics covered here.
A thanks to the head of innovation at Amadeus for insight into the industry split on the topic of collapsing margins. Finally we acknowledge the power and use of generative AI technologies for in depth research, rapid prototyping and surfacing of key topics and niches.
\subsection{Future Works and Next Steps}
Identify the research gaps here and potential business implications and setup of business + Proposed extensions and a long term agenda.
During the eights months of research dedicated to this work, a plethora of opportunities and industry gaps was identified, sadly a majority of which could not be addressed directly.

View File

@@ -0,0 +1,131 @@
import pandas as pd
import json
import numpy as np
import sys
import os
def process_supra(input_file, output_file):
print(f"Processing {input_file} -> {output_file}")
# Read the CSV
try:
# The CSV has a weird format: "Step","giddy-deluge-6 - distributions/prices"
# The header is on line 1.
# Let's verify the file content format first effectively.
# The previous read showed standard CSV with quoted fields.
df = pd.read_csv(input_file, quotechar='"', skipinitialspace=True)
except Exception as e:
print(f"Error reading CSV: {e}")
return
# Prepare for re-binning
# We need a common set of bins to plot a heatmap (surface)
# First, let's collect all data to determine range
all_min = float("inf")
all_max = float("-inf")
parsed_data = []
# The column names might be dynamic, so let's rely on indices
# Column 0: Step
# Column 1: JSON blob
for index, row in df.iterrows():
try:
step = int(row.iloc[0])
json_str = row.iloc[1]
# Cleaning potential double quotes issue if pandas didn't catch it perfect
# but pandas read_csv usually handles standard CSV escaping well.
data = json.loads(json_str)
bins = np.array(data["bins"])
values = np.array(data["values"])
# Update global range
if bins.min() < all_min:
all_min = bins.min()
if bins.max() > all_max:
all_max = bins.max()
parsed_data.append({"step": step, "bins": bins, "values": values})
except Exception as e:
print(f"Skipping row {index} due to error: {e}")
continue
if not parsed_data:
print("No data parsed.")
return
print(f"Found {len(parsed_data)} steps. Range: {all_min} to {all_max}")
# Define common grid
# Y-axis (Price)
# Using 100 bins for resolution
y_bins_edges = np.linspace(all_min, all_max, 101)
y_bin_centers = (y_bins_edges[:-1] + y_bins_edges[1:]) / 2
# Open output file
with open(output_file, "w") as f:
# PGFPlots 3D format often prefers no header or a specific header.
# We will use named columns.
f.write("step,price,density\n")
# Sort by step to ensure correct mesh ordering
parsed_data.sort(key=lambda x: x["step"])
for item in parsed_data:
step = item["step"]
original_bins = item["bins"]
original_values = item["values"]
# Re-binning logic
current_new_hist = np.zeros(len(y_bin_centers))
for i, (new_start, new_end) in enumerate(
zip(y_bins_edges[:-1], y_bins_edges[1:])
):
val = 0.0
# This inner loop is slightly inefficient O(N*M) but N~3000, M~100 -> 300k ops, totally fine.
for j in range(len(original_values)):
b_start = original_bins[j]
# Handle cases where values array might be 1 shorter than bins (histogram edges vs centers)
# The provided JSON has "bins" array larger than "values" by 1 usually for edges.
if j + 1 >= len(original_bins):
break
b_end = original_bins[j + 1]
b_width = b_end - b_start
if b_width <= 0:
continue
# Calculate overlap
overlap_start = max(new_start, b_start)
overlap_end = min(new_end, b_end)
overlap = max(0, overlap_end - overlap_start)
if overlap > 0:
# Add proportional count
val += original_values[j] * (overlap / b_width)
current_new_hist[i] = val
# Write row to file for this step
for price, density in zip(y_bin_centers, current_new_hist):
# PGFPlots expects x y z
f.write(f"{step},{price},{density}\n")
# Add a blank line for PGFPlots matrix format (essential for 'mesh' or 'surf')
f.write("\n")
if __name__ == "__main__":
# Resolve relative paths relative to where script is run, or use absolute
base_dir = os.path.dirname(os.path.abspath(__file__))
input_path = os.path.join(base_dir, "supra.csv")
output_path = os.path.join(base_dir, "supra_data.csv")
process_supra(input_path, output_path)

View File

@@ -0,0 +1,41 @@
"Step","giddy-deluge-6 - distributions/prices"
"100","{""values"":[2,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1],""_type"":""histogram"",""bins"":[15.76888656616211,17.813893377780914,19.85890018939972,21.903907001018524,23.94891381263733,25.993920624256134,28.03892743587494,30.083934247493744,32.12894105911255,34.173947870731354,36.21895468235016,38.263961493968964,40.30896830558777,42.35397511720657,44.39898192882538,46.44398874044418,48.48899555206299,50.53400236368179,52.5790091753006,54.6240159869194,56.66902279853821,58.71402961015701,60.75903642177582,62.80404323339462,64.84905004501343,66.89405685663223,68.93906366825104,70.98407047986984,73.02907729148865,75.07408410310745,77.11909091472626,79.16409772634506,81.20910453796387,83.25411134958267,85.29911816120148,87.34412497282028,89.38913178443909,91.43413859605789,93.4791454076767,95.5241522192955,97.5691590309143,99.61416584253311,101.65917265415192,103.70417946577072,105.74918627738953,107.79419308900833,109.83919990062714,111.88420671224594,113.92921352386475,115.97422033548355,118.01922714710236,120.06423395872116,122.10924077033997,124.15424758195877,126.19925439357758,128.24426120519638,130.28926801681519,132.334274828434,134.3792816400528,136.4242884516716,138.4692952632904,140.5143020749092,142.55930888652802,144.60431569814682,146.64932250976562]}"
"200","{""_type"":""histogram"",""values"":[1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3],""bins"":[10.439504623413086,12.620137363672256,14.800770103931427,16.981402844190598,19.162035584449768,21.34266832470894,23.52330106496811,25.70393380522728,27.88456654548645,30.06519928574562,32.24583202600479,34.42646476626396,36.60709750652313,38.7877302467823,40.96836298704147,43.148995727300644,45.329628467559814,47.510261207818985,49.690893948078156,51.871526688337326,54.0521594285965,56.23279216885567,58.41342490911484,60.59405764937401,62.77469038963318,64.95532312989235,67.13595587015152,69.31658861041069,71.49722135066986,73.67785409092903,75.8584868311882,78.03911957144737,80.21975231170654,82.40038505196571,84.58101779222488,86.76165053248405,88.94228327274323,91.1229160130024,93.30354875326157,95.48418149352074,97.66481423377991,99.84544697403908,102.02607971429825,104.20671245455742,106.38734519481659,108.56797793507576,110.74861067533493,112.9292434155941,115.10987615585327,117.29050889611244,119.47114163637161,121.65177437663078,123.83240711688995,126.01303985714912,128.1936725974083,130.37430533766747,132.55493807792664,134.7355708181858,136.91620355844498,139.09683629870415,141.27746903896332,143.4581017792225,145.63873451948166,147.81936725974083,150]}"
"300","{""bins"":[92.91828918457031,93.81018829345703,94.70209503173828,95.593994140625,96.48589324951172,97.37779998779297,98.26969909667969,99.1615982055664,100.05350494384766,100.94540405273438,101.83731079101562,102.72920989990234,103.62110900878906,104.51301574707031,105.40491485595703,106.29681396484375,107.188720703125,108.08061981201172,108.97251892089844,109.86442565917969,110.7563247680664,111.64822387695312,112.54013061523438,113.4320297241211,114.32392883300781,115.21583557128906,116.10773468017578,116.9996337890625,117.89154052734375,118.78343963623047,119.67533874511719,120.56724548339844,121.45914459228516,122.35104370117188,123.24295043945312,124.13484954833984,125.02674865722656,125.91865539550781,126.81055450439453,127.70245361328125,128.5943603515625,129.48626708984375,130.37815856933594,131.2700653076172,132.16195678710938,133.05386352539062,133.94577026367188,134.83767700195312,135.7295684814453,136.62147521972656,137.51336669921875,138.4052734375,139.29718017578125,140.1890869140625,141.0809783935547,141.97288513183594,142.86477661132812,143.75668334960938,144.64859008789062,145.54049682617188,146.43238830566406,147.3242950439453,148.21620178222656,149.10809326171875,150],""_type"":""histogram"",""values"":[1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,1,0,3]}"
"400","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,3,0,0,0,3],""bins"":[141.8555450439453,141.98280334472656,142.1100616455078,142.23731994628906,142.3645782470703,142.49183654785156,142.6190948486328,142.746337890625,142.87359619140625,143.0008544921875,143.12811279296875,143.25537109375,143.38262939453125,143.5098876953125,143.63714599609375,143.764404296875,143.89166259765625,144.0189208984375,144.14617919921875,144.2734375,144.4006805419922,144.52793884277344,144.6551971435547,144.78245544433594,144.9097137451172,145.03697204589844,145.1642303466797,145.29148864746094,145.4187469482422,145.54600524902344,145.6732635498047,145.80052185058594,145.92776489257812,146.05502319335938,146.18228149414062,146.30953979492188,146.43679809570312,146.56405639648438,146.69131469726562,146.81857299804688,146.94583129882812,147.07308959960938,147.20034790039062,147.32760620117188,147.45486450195312,147.58212280273438,147.70936584472656,147.8366241455078,147.96388244628906,148.0911407470703,148.21839904785156,148.3456573486328,148.47291564941406,148.6001739501953,148.72743225097656,148.8546905517578,148.98194885253906,149.1092071533203,149.2364501953125,149.36370849609375,149.490966796875,149.61822509765625,149.7454833984375,149.87274169921875,150],""_type"":""histogram""}"
"500","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,3],""bins"":[142.30267333984375,142.42294311523438,142.543212890625,142.66348266601562,142.78375244140625,142.90402221679688,143.0242919921875,143.14456176757812,143.26483154296875,143.38511657714844,143.50538635253906,143.6256561279297,143.7459259033203,143.86619567871094,143.98646545410156,144.1067352294922,144.2270050048828,144.34727478027344,144.46754455566406,144.5878143310547,144.7080841064453,144.82835388183594,144.94862365722656,145.0688934326172,145.18917846679688,145.3094482421875,145.42971801757812,145.54998779296875,145.67025756835938,145.79052734375,145.91079711914062,146.03106689453125,146.15133666992188,146.2716064453125,146.39187622070312,146.51214599609375,146.63241577148438,146.752685546875,146.87295532226562,146.99322509765625,147.11349487304688,147.23377990722656,147.3540496826172,147.4743194580078,147.59458923339844,147.71485900878906,147.8351287841797,147.9553985595703,148.07566833496094,148.19593811035156,148.3162078857422,148.4364776611328,148.55674743652344,148.67701721191406,148.7972869873047,148.9175567626953,149.037841796875,149.15811157226562,149.27838134765625,149.39865112304688,149.5189208984375,149.63919067382812,149.75946044921875,149.87973022460938,150]}"
"600","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,1,0,1,0,0,0,0,3],""bins"":[143.02142333984375,143.13046264648438,143.239501953125,143.34854125976562,143.45758056640625,143.56661987304688,143.6756591796875,143.78469848632812,143.89373779296875,144.00279235839844,144.11183166503906,144.2208709716797,144.3299102783203,144.43894958496094,144.54798889160156,144.6570281982422,144.7660675048828,144.87510681152344,144.98414611816406,145.0931854248047,145.2022247314453,145.31126403808594,145.42030334472656,145.5293426513672,145.63839721679688,145.7474365234375,145.85647583007812,145.96551513671875,146.07455444335938,146.18359375,146.29263305664062,146.40167236328125,146.51071166992188,146.6197509765625,146.72879028320312,146.83782958984375,146.94686889648438,147.055908203125,147.16494750976562,147.27398681640625,147.38302612304688,147.49208068847656,147.6011199951172,147.7101593017578,147.81919860839844,147.92823791503906,148.0372772216797,148.1463165283203,148.25535583496094,148.36439514160156,148.4734344482422,148.5824737548828,148.69151306152344,148.80055236816406,148.9095916748047,149.0186309814453,149.127685546875,149.23672485351562,149.34576416015625,149.45480346679688,149.5638427734375,149.67288208007812,149.78192138671875,149.89096069335938,150],""_type"":""histogram""}"
"700","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,1,3],""bins"":[147.6887969970703,147.72491455078125,147.76101684570312,147.79713439941406,147.833251953125,147.86935424804688,147.9054718017578,147.94158935546875,147.97769165039062,148.01380920410156,148.0499267578125,148.08602905273438,148.1221466064453,148.15826416015625,148.19436645507812,148.23048400878906,148.2666015625,148.30270385742188,148.3388214111328,148.37493896484375,148.41104125976562,148.44715881347656,148.4832763671875,148.51937866210938,148.5554962158203,148.59161376953125,148.62771606445312,148.66383361816406,148.699951171875,148.73605346679688,148.7721710205078,148.80828857421875,148.84439086914062,148.88050842285156,148.9166259765625,148.95274353027344,148.9888458251953,149.02496337890625,149.0610809326172,149.09718322753906,149.13330078125,149.16941833496094,149.2055206298828,149.24163818359375,149.2777557373047,149.31385803222656,149.3499755859375,149.38609313964844,149.4221954345703,149.45831298828125,149.4944305419922,149.53053283691406,149.566650390625,149.60276794433594,149.6388702392578,149.67498779296875,149.7111053466797,149.74720764160156,149.7833251953125,149.81944274902344,149.8555450439453,149.89166259765625,149.9277801513672,149.96388244628906,150]}"
"800","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,3],""bins"":[149.21865844726562,149.23086547851562,149.24307250976562,149.25527954101562,149.26748657226562,149.27969360351562,149.2919158935547,149.3041229248047,149.3163299560547,149.3285369873047,149.3407440185547,149.3529510498047,149.3651580810547,149.3773651123047,149.3895721435547,149.4017791748047,149.41400146484375,149.42620849609375,149.43841552734375,149.45062255859375,149.46282958984375,149.47503662109375,149.48724365234375,149.49945068359375,149.51165771484375,149.52386474609375,149.53607177734375,149.5482940673828,149.5605010986328,149.5727081298828,149.5849151611328,149.5971221923828,149.6093292236328,149.6215362548828,149.6337432861328,149.6459503173828,149.6581573486328,149.6703643798828,149.68258666992188,149.69479370117188,149.70700073242188,149.71920776367188,149.73141479492188,149.74362182617188,149.75582885742188,149.76803588867188,149.78024291992188,149.79244995117188,149.80465698242188,149.81687927246094,149.82908630371094,149.84129333496094,149.85350036621094,149.86570739746094,149.87791442871094,149.89012145996094,149.90232849121094,149.91453552246094,149.92674255371094,149.93896484375,149.951171875,149.96337890625,149.9755859375,149.98779296875,150]}"
"900","{""_type"":""histogram"",""values"":[1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,3],""bins"":[149.2405548095703,149.25242614746094,149.2642822265625,149.27615356445312,149.28802490234375,149.2998809814453,149.31175231933594,149.32362365722656,149.33547973632812,149.34735107421875,149.35922241210938,149.37107849121094,149.38294982910156,149.3948211669922,149.40667724609375,149.41854858398438,149.430419921875,149.44227600097656,149.4541473388672,149.4660186767578,149.47787475585938,149.48974609375,149.50161743164062,149.5134735107422,149.5253448486328,149.53721618652344,149.549072265625,149.56094360351562,149.57281494140625,149.5846710205078,149.59654235839844,149.60841369628906,149.62026977539062,149.63214111328125,149.64401245117188,149.6558837890625,149.66773986816406,149.6796112060547,149.6914825439453,149.70333862304688,149.7152099609375,149.72708129882812,149.7389373779297,149.7508087158203,149.76268005371094,149.7745361328125,149.78640747070312,149.79827880859375,149.8101348876953,149.82200622558594,149.83387756347656,149.84573364257812,149.85760498046875,149.86947631835938,149.88133239746094,149.89320373535156,149.9050750732422,149.91693115234375,149.92880249023438,149.940673828125,149.95252990722656,149.9644012451172,149.9762725830078,149.98812866210938,150]}"
"1000","{""bins"":[149.04977416992188,149.0646209716797,149.0794677734375,149.0943145751953,149.10916137695312,149.12400817871094,149.13885498046875,149.15370178222656,149.16854858398438,149.1833953857422,149.1982421875,149.2130889892578,149.22793579101562,149.24278259277344,149.25762939453125,149.27247619628906,149.28732299804688,149.30218505859375,149.31703186035156,149.33187866210938,149.3467254638672,149.361572265625,149.3764190673828,149.39126586914062,149.40611267089844,149.42095947265625,149.43580627441406,149.45065307617188,149.4654998779297,149.4803466796875,149.4951934814453,149.51004028320312,149.52488708496094,149.53973388671875,149.55458068847656,149.56942749023438,149.5842742919922,149.59912109375,149.6139678955078,149.62881469726562,149.64366149902344,149.65850830078125,149.67335510253906,149.68820190429688,149.7030487060547,149.7178955078125,149.7327423095703,149.74758911132812,149.762451171875,149.7772979736328,149.79214477539062,149.80699157714844,149.82183837890625,149.83668518066406,149.85153198242188,149.8663787841797,149.8812255859375,149.8960723876953,149.91091918945312,149.92576599121094,149.94061279296875,149.95545959472656,149.97030639648438,149.9851531982422,150],""_type"":""histogram"",""values"":[1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,3]}"
"1100","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,1,0,0,0,0,3],""bins"":[149.34432983398438,149.3545684814453,149.3648223876953,149.37506103515625,149.38531494140625,149.3955535888672,149.40579223632812,149.41604614257812,149.42628479003906,149.43653869628906,149.44677734375,149.45701599121094,149.46726989746094,149.47750854492188,149.48776245117188,149.4980010986328,149.50823974609375,149.51849365234375,149.5287322998047,149.5389862060547,149.54922485351562,149.55947875976562,149.56971740722656,149.5799560546875,149.5902099609375,149.60044860839844,149.61070251464844,149.62094116210938,149.6311798095703,149.6414337158203,149.65167236328125,149.66192626953125,149.6721649169922,149.68240356445312,149.69265747070312,149.70289611816406,149.71315002441406,149.723388671875,149.73362731933594,149.74388122558594,149.75411987304688,149.76437377929688,149.7746124267578,149.78485107421875,149.79510498046875,149.8053436279297,149.8155975341797,149.82583618164062,149.83609008789062,149.84632873535156,149.8565673828125,149.8668212890625,149.87705993652344,149.88731384277344,149.89755249023438,149.9077911376953,149.9180450439453,149.92828369140625,149.93853759765625,149.9487762451172,149.95901489257812,149.96926879882812,149.97950744628906,149.98976135253906,150]}"
"1200","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,3],""bins"":[148.93704223632812,148.95364379882812,148.9702606201172,148.9868621826172,149.00347900390625,149.02008056640625,149.0366973876953,149.0532989501953,149.06991577148438,149.08651733398438,149.10313415527344,149.11973571777344,149.1363525390625,149.1529541015625,149.16957092285156,149.18617248535156,149.20278930664062,149.21939086914062,149.23599243164062,149.2526092529297,149.2692108154297,149.28582763671875,149.30242919921875,149.3190460205078,149.3356475830078,149.35226440429688,149.36886596679688,149.38548278808594,149.40208435058594,149.418701171875,149.435302734375,149.45191955566406,149.46852111816406,149.48512268066406,149.50173950195312,149.51834106445312,149.5349578857422,149.5515594482422,149.56817626953125,149.58477783203125,149.6013946533203,149.6179962158203,149.63461303710938,149.65121459960938,149.66783142089844,149.68443298339844,149.7010498046875,149.7176513671875,149.7342529296875,149.75086975097656,149.76747131347656,149.78408813476562,149.80068969726562,149.8173065185547,149.8339080810547,149.85052490234375,149.86712646484375,149.8837432861328,149.9003448486328,149.91696166992188,149.93356323242188,149.95018005371094,149.96678161621094,149.9833984375,150],""_type"":""histogram""}"
"1300","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,3],""bins"":[149.0462646484375,149.06117248535156,149.07606506347656,149.09097290039062,149.10586547851562,149.1207733154297,149.13568115234375,149.15057373046875,149.1654815673828,149.18038940429688,149.19528198242188,149.21018981933594,149.22509765625,149.239990234375,149.25489807128906,149.26979064941406,149.28469848632812,149.2996063232422,149.3144989013672,149.32940673828125,149.34429931640625,149.3592071533203,149.37411499023438,149.38900756835938,149.40391540527344,149.4188232421875,149.4337158203125,149.44862365722656,149.46353149414062,149.47842407226562,149.4933319091797,149.5082244873047,149.52313232421875,149.5380401611328,149.5529327392578,149.56784057617188,149.58273315429688,149.59764099121094,149.612548828125,149.62744140625,149.64234924316406,149.65725708007812,149.67214965820312,149.6870574951172,149.70196533203125,149.71685791015625,149.7317657470703,149.7466583251953,149.76156616210938,149.77647399902344,149.79136657714844,149.8062744140625,149.8211669921875,149.83607482910156,149.85098266601562,149.86587524414062,149.8807830810547,149.89569091796875,149.91058349609375,149.9254913330078,149.94039916992188,149.95529174804688,149.97019958496094,149.98509216308594,150]}"
"1400","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,4],""bins"":[148.76895141601562,148.78819274902344,148.8074188232422,148.82666015625,148.84588623046875,148.86512756347656,148.88436889648438,148.90359497070312,148.92283630371094,148.9420623779297,148.9613037109375,148.9805450439453,148.99977111816406,149.01901245117188,149.03823852539062,149.05747985839844,149.07672119140625,149.095947265625,149.1151885986328,149.13441467285156,149.15365600585938,149.17288208007812,149.19212341308594,149.21136474609375,149.2305908203125,149.2498321533203,149.26905822753906,149.28829956054688,149.3075408935547,149.32676696777344,149.34600830078125,149.365234375,149.3844757080078,149.40371704101562,149.42294311523438,149.4421844482422,149.46141052246094,149.48065185546875,149.49989318847656,149.5191192626953,149.53836059570312,149.55758666992188,149.5768280029297,149.5960693359375,149.61529541015625,149.63453674316406,149.6537628173828,149.67300415039062,149.69223022460938,149.7114715576172,149.730712890625,149.74993896484375,149.76918029785156,149.7884063720703,149.80764770507812,149.82688903808594,149.8461151123047,149.8653564453125,149.88458251953125,149.90382385253906,149.92306518554688,149.94229125976562,149.96153259277344,149.9807586669922,150]}"
"1500","{""bins"":[149.55426025390625,149.56121826171875,149.5681915283203,149.5751495361328,149.58212280273438,149.58908081054688,149.59605407714844,149.60301208496094,149.6099853515625,149.616943359375,149.6239013671875,149.63087463378906,149.63783264160156,149.64480590820312,149.65176391601562,149.6587371826172,149.6656951904297,149.6726531982422,149.67962646484375,149.68658447265625,149.6935577392578,149.7005157470703,149.70748901367188,149.71444702148438,149.72140502929688,149.72837829589844,149.73533630371094,149.7423095703125,149.749267578125,149.75624084472656,149.76319885253906,149.77017211914062,149.77713012695312,149.78408813476562,149.7910614013672,149.7980194091797,149.80499267578125,149.81195068359375,149.8189239501953,149.8258819580078,149.83285522460938,149.83981323242188,149.84677124023438,149.85374450683594,149.86070251464844,149.86767578125,149.8746337890625,149.88160705566406,149.88856506347656,149.89552307128906,149.90249633789062,149.90945434570312,149.9164276123047,149.9233856201172,149.93035888671875,149.93731689453125,149.94427490234375,149.9512481689453,149.9582061767578,149.96517944335938,149.97213745117188,149.97911071777344,149.98606872558594,149.9930419921875,150],""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,3]}"
"1600","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,3],""bins"":[149.17001342773438,149.1829833984375,149.19595336914062,149.20892333984375,149.22189331054688,149.23486328125,149.24781799316406,149.2607879638672,149.2737579345703,149.28672790527344,149.29969787597656,149.3126678466797,149.3256378173828,149.33860778808594,149.35157775878906,149.3645477294922,149.37750244140625,149.39047241210938,149.4034423828125,149.41641235351562,149.42938232421875,149.44235229492188,149.455322265625,149.46829223632812,149.48126220703125,149.49423217773438,149.5072021484375,149.52015686035156,149.5331268310547,149.5460968017578,149.55906677246094,149.57203674316406,149.5850067138672,149.5979766845703,149.61094665527344,149.62391662597656,149.6368865966797,149.6498565673828,149.66281127929688,149.67578125,149.68875122070312,149.70172119140625,149.71469116210938,149.7276611328125,149.74063110351562,149.75360107421875,149.76657104492188,149.779541015625,149.79251098632812,149.8054656982422,149.8184356689453,149.83140563964844,149.84437561035156,149.8573455810547,149.8703155517578,149.88328552246094,149.89625549316406,149.9092254638672,149.9221954345703,149.93515014648438,149.9481201171875,149.96109008789062,149.97406005859375,149.98703002929688,150]}"
"1700","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,3],""bins"":[149.36141967773438,149.37139892578125,149.38137817382812,149.391357421875,149.40133666992188,149.41131591796875,149.42127990722656,149.43125915527344,149.4412384033203,149.4512176513672,149.46119689941406,149.47117614746094,149.4811553955078,149.4911346435547,149.50111389160156,149.51109313964844,149.52105712890625,149.53103637695312,149.541015625,149.55099487304688,149.56097412109375,149.57095336914062,149.5809326171875,149.59091186523438,149.60089111328125,149.61087036132812,149.620849609375,149.6308135986328,149.6407928466797,149.65077209472656,149.66075134277344,149.6707305908203,149.6807098388672,149.69068908691406,149.70066833496094,149.7106475830078,149.7206268310547,149.73060607910156,149.74057006835938,149.75054931640625,149.76052856445312,149.7705078125,149.78048706054688,149.79046630859375,149.80044555664062,149.8104248046875,149.82040405273438,149.83038330078125,149.84036254882812,149.85032653808594,149.8603057861328,149.8702850341797,149.88026428222656,149.89024353027344,149.9002227783203,149.9102020263672,149.92018127441406,149.93016052246094,149.9401397705078,149.95010375976562,149.9600830078125,149.97006225585938,149.98004150390625,149.99002075195312,150]}"
"1800","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,3],""bins"":[149.42300415039062,149.43202209472656,149.4410400390625,149.45005798339844,149.4590606689453,149.46807861328125,149.4770965576172,149.48611450195312,149.49513244628906,149.504150390625,149.51315307617188,149.5221710205078,149.53118896484375,149.5402069091797,149.54922485351562,149.55824279785156,149.5672607421875,149.57626342773438,149.5852813720703,149.59429931640625,149.6033172607422,149.61233520507812,149.62135314941406,149.63035583496094,149.63937377929688,149.6483917236328,149.65740966796875,149.6664276123047,149.67544555664062,149.6844482421875,149.69346618652344,149.70248413085938,149.7115020751953,149.72052001953125,149.7295379638672,149.73855590820312,149.74755859375,149.75657653808594,149.76559448242188,149.7746124267578,149.78363037109375,149.7926483154297,149.80165100097656,149.8106689453125,149.81968688964844,149.82870483398438,149.8377227783203,149.84674072265625,149.85574340820312,149.86476135253906,149.873779296875,149.88279724121094,149.89181518554688,149.9008331298828,149.90985107421875,149.91885375976562,149.92787170410156,149.9368896484375,149.94590759277344,149.95492553710938,149.9639434814453,149.9729461669922,149.98196411132812,149.99098205566406,150],""_type"":""histogram""}"
"1900","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,3],""bins"":[148.7623748779297,148.78170776367188,148.80105590820312,148.8203887939453,148.8397216796875,148.85906982421875,148.87840270996094,148.89773559570312,148.91708374023438,148.93641662597656,148.95574951171875,148.97509765625,148.9944305419922,149.01376342773438,149.03311157226562,149.0524444580078,149.07177734375,149.09112548828125,149.11045837402344,149.12979125976562,149.14913940429688,149.16847229003906,149.18780517578125,149.2071533203125,149.2264862060547,149.24581909179688,149.26516723632812,149.2845001220703,149.3038330078125,149.32318115234375,149.34251403808594,149.36184692382812,149.38119506835938,149.40052795410156,149.41986083984375,149.43919372558594,149.4585418701172,149.47787475585938,149.49720764160156,149.5165557861328,149.535888671875,149.5552215576172,149.57456970214844,149.59390258789062,149.6132354736328,149.63258361816406,149.65191650390625,149.67124938964844,149.6905975341797,149.70993041992188,149.72926330566406,149.7486114501953,149.7679443359375,149.7872772216797,149.80662536621094,149.82595825195312,149.8452911376953,149.86463928222656,149.88397216796875,149.90330505371094,149.9226531982422,149.94198608398438,149.96131896972656,149.9806671142578,150]}"
"2000","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,2,1,0,0,0,0,1,0,0,0,0,0,1,3],""bins"":[149.5408477783203,149.5480194091797,149.55519104003906,149.5623779296875,149.56954956054688,149.57672119140625,149.58389282226562,149.591064453125,149.59823608398438,149.6054229736328,149.6125946044922,149.61976623535156,149.62693786621094,149.6341094970703,149.6412811279297,149.64846801757812,149.6556396484375,149.66281127929688,149.66998291015625,149.67715454101562,149.684326171875,149.69151306152344,149.6986846923828,149.7058563232422,149.71302795410156,149.72019958496094,149.7273712158203,149.73455810546875,149.74172973632812,149.7489013671875,149.75607299804688,149.76324462890625,149.77041625976562,149.77760314941406,149.78477478027344,149.7919464111328,149.7991180419922,149.80628967285156,149.8134765625,149.82064819335938,149.82781982421875,149.83499145507812,149.8421630859375,149.84933471679688,149.8565216064453,149.8636932373047,149.87086486816406,149.87803649902344,149.8852081298828,149.8923797607422,149.89956665039062,149.90673828125,149.91390991210938,149.92108154296875,149.92825317382812,149.9354248046875,149.94261169433594,149.9497833251953,149.9569549560547,149.96412658691406,149.97129821777344,149.9784698486328,149.98565673828125,149.99282836914062,150]}"
"2100","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,3],""bins"":[149.5018310546875,149.50961303710938,149.51739501953125,149.52517700195312,149.532958984375,149.54075622558594,149.5485382080078,149.5563201904297,149.56410217285156,149.57188415527344,149.5796661376953,149.5874481201172,149.59524536132812,149.60302734375,149.61080932617188,149.61859130859375,149.62637329101562,149.6341552734375,149.64193725585938,149.64971923828125,149.65750122070312,149.66529846191406,149.67308044433594,149.6808624267578,149.6886444091797,149.69642639160156,149.70420837402344,149.7119903564453,149.71978759765625,149.72756958007812,149.7353515625,149.74313354492188,149.75091552734375,149.75869750976562,149.7664794921875,149.77426147460938,149.78204345703125,149.7898406982422,149.79762268066406,149.80540466308594,149.8131866455078,149.8209686279297,149.82875061035156,149.83653259277344,149.84432983398438,149.85211181640625,149.85989379882812,149.86767578125,149.87545776367188,149.88323974609375,149.89102172851562,149.8988037109375,149.90658569335938,149.9143829345703,149.9221649169922,149.92994689941406,149.93772888183594,149.9455108642578,149.9532928466797,149.96107482910156,149.9688720703125,149.97665405273438,149.98443603515625,149.99221801757812,150]}"
"2200","{""bins"":[149.558349609375,149.56524658203125,149.5721435546875,149.5790557861328,149.58595275878906,149.5928497314453,149.59976196289062,149.60665893554688,149.61355590820312,149.62045288085938,149.62734985351562,149.63426208496094,149.6411590576172,149.64805603027344,149.65496826171875,149.661865234375,149.66876220703125,149.6756591796875,149.68255615234375,149.68946838378906,149.6963653564453,149.70326232910156,149.71017456054688,149.71707153320312,149.72396850585938,149.73086547851562,149.73776245117188,149.7446746826172,149.75157165527344,149.7584686279297,149.765380859375,149.77227783203125,149.7791748046875,149.78607177734375,149.79296875,149.7998809814453,149.80677795410156,149.8136749267578,149.82058715820312,149.82748413085938,149.83438110351562,149.84127807617188,149.84817504882812,149.85508728027344,149.8619842529297,149.86888122558594,149.87579345703125,149.8826904296875,149.88958740234375,149.896484375,149.90338134765625,149.91029357910156,149.9171905517578,149.92408752441406,149.93099975585938,149.93789672851562,149.94479370117188,149.95169067382812,149.95858764648438,149.9654998779297,149.97239685058594,149.9792938232422,149.9862060546875,149.99310302734375,150],""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,2,1,1,0,0,0,3]}"
"2300","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,2,1,0,0,3],""bins"":[149.1126251220703,149.12649536132812,149.14035034179688,149.1542205810547,149.1680908203125,149.18194580078125,149.19581604003906,149.20968627929688,149.22354125976562,149.23741149902344,149.25128173828125,149.26513671875,149.2790069580078,149.29287719726562,149.30673217773438,149.3206024169922,149.33447265625,149.34832763671875,149.36219787597656,149.37606811523438,149.38992309570312,149.40379333496094,149.41766357421875,149.4315185546875,149.4453887939453,149.45925903320312,149.47311401367188,149.4869842529297,149.5008544921875,149.51470947265625,149.52857971191406,149.54244995117188,149.55630493164062,149.57017517089844,149.58404541015625,149.59791564941406,149.6117706298828,149.62564086914062,149.63951110839844,149.6533660888672,149.667236328125,149.6811065673828,149.69496154785156,149.70883178710938,149.7227020263672,149.73655700683594,149.75042724609375,149.76429748535156,149.7781524658203,149.79202270507812,149.80589294433594,149.8197479248047,149.8336181640625,149.8474884033203,149.86134338378906,149.87521362304688,149.8890838623047,149.90293884277344,149.91680908203125,149.93067932128906,149.9445343017578,149.95840454101562,149.97227478027344,149.9861297607422,150]}"
"2400","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,3],""bins"":[149.71124267578125,149.71575927734375,149.7202606201172,149.7247772216797,149.7292938232422,149.73379516601562,149.73831176757812,149.74282836914062,149.74734497070312,149.75184631347656,149.75636291503906,149.76087951660156,149.765380859375,149.7698974609375,149.7744140625,149.77891540527344,149.78343200683594,149.78794860839844,149.79244995117188,149.79696655273438,149.80148315429688,149.8059844970703,149.8105010986328,149.8150177001953,149.81951904296875,149.82403564453125,149.82855224609375,149.83306884765625,149.8375701904297,149.8420867919922,149.8466033935547,149.85110473632812,149.85562133789062,149.86013793945312,149.86463928222656,149.86915588378906,149.87367248535156,149.878173828125,149.8826904296875,149.88720703125,149.8917236328125,149.89622497558594,149.90074157714844,149.90525817871094,149.90975952148438,149.91427612304688,149.91879272460938,149.9232940673828,149.9278106689453,149.9323272705078,149.93682861328125,149.94134521484375,149.94586181640625,149.9503631591797,149.9548797607422,149.9593963623047,149.96389770507812,149.96841430664062,149.97293090820312,149.97744750976562,149.98194885253906,149.98646545410156,149.99098205566406,149.9954833984375,150]}"
"2500","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,0,0,3],""bins"":[149.58355712890625,149.59005737304688,149.59657287597656,149.6030731201172,149.60958862304688,149.6160888671875,149.6226043701172,149.6291046142578,149.6356201171875,149.64212036132812,149.64862060546875,149.65513610839844,149.66163635253906,149.66815185546875,149.67465209960938,149.68116760253906,149.6876678466797,149.6941680908203,149.70068359375,149.70718383789062,149.7136993408203,149.72019958496094,149.72671508789062,149.73321533203125,149.73971557617188,149.74623107910156,149.7527313232422,149.75924682617188,149.7657470703125,149.7722625732422,149.7787628173828,149.7852783203125,149.79177856445312,149.79827880859375,149.80479431152344,149.81129455566406,149.81781005859375,149.82431030273438,149.83082580566406,149.8373260498047,149.84384155273438,149.850341796875,149.85684204101562,149.8633575439453,149.86985778808594,149.87637329101562,149.88287353515625,149.88938903808594,149.89588928222656,149.9023895263672,149.90890502929688,149.9154052734375,149.9219207763672,149.9284210205078,149.9349365234375,149.94143676757812,149.94793701171875,149.95445251464844,149.96095275878906,149.96746826171875,149.97396850585938,149.98048400878906,149.9869842529297,149.99349975585938,150],""_type"":""histogram""}"
"2600","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,3],""bins"":[149.67474365234375,149.67982482910156,149.68490600585938,149.6899871826172,149.695068359375,149.7001495361328,149.70523071289062,149.71031188964844,149.71539306640625,149.72048950195312,149.72557067871094,149.73065185546875,149.73573303222656,149.74081420898438,149.7458953857422,149.7509765625,149.7560577392578,149.76113891601562,149.76622009277344,149.77130126953125,149.77638244628906,149.78146362304688,149.7865447998047,149.7916259765625,149.79672241210938,149.8018035888672,149.806884765625,149.8119659423828,149.81704711914062,149.82212829589844,149.82720947265625,149.83229064941406,149.83737182617188,149.8424530029297,149.8475341796875,149.8526153564453,149.85769653320312,149.86277770996094,149.86785888671875,149.87294006347656,149.87802124023438,149.88311767578125,149.88819885253906,149.89328002929688,149.8983612060547,149.9034423828125,149.9085235595703,149.91360473632812,149.91868591308594,149.92376708984375,149.92884826660156,149.93392944335938,149.9390106201172,149.944091796875,149.9491729736328,149.95425415039062,149.9593505859375,149.9644317626953,149.96951293945312,149.97459411621094,149.97967529296875,149.98475646972656,149.98983764648438,149.9949188232422,150]}"
"2700","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,3],""_type"":""histogram"",""bins"":[149.65234375,149.65777587890625,149.6632080078125,149.66864013671875,149.674072265625,149.67950439453125,149.6849365234375,149.69036865234375,149.69580078125,149.70123291015625,149.7066650390625,149.71209716796875,149.717529296875,149.72296142578125,149.7283935546875,149.73382568359375,149.7392578125,149.74468994140625,149.7501220703125,149.75555419921875,149.760986328125,149.76641845703125,149.7718505859375,149.77728271484375,149.78271484375,149.78814697265625,149.7935791015625,149.79901123046875,149.804443359375,149.80987548828125,149.8153076171875,149.82073974609375,149.826171875,149.83160400390625,149.8370361328125,149.84246826171875,149.847900390625,149.85333251953125,149.8587646484375,149.86419677734375,149.86962890625,149.87506103515625,149.8804931640625,149.88592529296875,149.891357421875,149.89678955078125,149.9022216796875,149.90765380859375,149.9130859375,149.91851806640625,149.9239501953125,149.92938232421875,149.934814453125,149.94024658203125,149.9456787109375,149.95111083984375,149.95654296875,149.96197509765625,149.9674072265625,149.97283935546875,149.978271484375,149.98370361328125,149.9891357421875,149.99456787109375,150]}"
"2800","{""bins"":[149.7410430908203,149.74508666992188,149.74913024902344,149.75318908691406,149.75723266601562,149.7612762451172,149.76531982421875,149.7693634033203,149.77340698242188,149.7774658203125,149.78150939941406,149.78555297851562,149.7895965576172,149.79364013671875,149.7976837158203,149.80174255371094,149.8057861328125,149.80982971191406,149.81387329101562,149.8179168701172,149.82196044921875,149.82601928710938,149.83006286621094,149.8341064453125,149.83815002441406,149.84219360351562,149.8462371826172,149.8502960205078,149.85433959960938,149.85838317871094,149.8624267578125,149.86647033691406,149.87051391601562,149.87457275390625,149.8786163330078,149.88265991210938,149.88670349121094,149.8907470703125,149.89480590820312,149.8988494873047,149.90289306640625,149.9069366455078,149.91098022460938,149.91502380371094,149.91908264160156,149.92312622070312,149.9271697998047,149.93121337890625,149.9352569580078,149.93930053710938,149.943359375,149.94740295410156,149.95144653320312,149.9554901123047,149.95953369140625,149.9635772705078,149.96763610839844,149.9716796875,149.97572326660156,149.97976684570312,149.9838104248047,149.98785400390625,149.99191284179688,149.99595642089844,150],""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,3]}"
"2900","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,3],""bins"":[149.65863037109375,149.66397094726562,149.66929626464844,149.6746368408203,149.67996215820312,149.685302734375,149.6906280517578,149.6959686279297,149.7012939453125,149.70663452148438,149.71197509765625,149.71730041503906,149.72264099121094,149.72796630859375,149.73330688476562,149.73863220214844,149.7439727783203,149.7493133544922,149.754638671875,149.75997924804688,149.7653045654297,149.77064514160156,149.77597045898438,149.78131103515625,149.78665161132812,149.79197692871094,149.7973175048828,149.80264282226562,149.8079833984375,149.8133087158203,149.8186492919922,149.823974609375,149.82931518554688,149.83465576171875,149.83998107910156,149.84532165527344,149.85064697265625,149.85598754882812,149.86131286621094,149.8666534423828,149.87197875976562,149.8773193359375,149.88265991210938,149.8879852294922,149.89332580566406,149.89865112304688,149.90399169921875,149.90931701660156,149.91465759277344,149.9199981689453,149.92532348632812,149.9306640625,149.9359893798828,149.9413299560547,149.9466552734375,149.95199584960938,149.95733642578125,149.96266174316406,149.96800231933594,149.97332763671875,149.97866821289062,149.98399353027344,149.9893341064453,149.99465942382812,150]}"
"3000","{""_type"":""histogram"",""values"":[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],""bins"":[149.756103515625,149.75991821289062,149.76373291015625,149.7675323486328,149.77134704589844,149.77516174316406,149.77896118164062,149.78277587890625,149.78659057617188,149.7904052734375,149.79421997070312,149.7980194091797,149.8018341064453,149.80564880371094,149.8094482421875,149.81326293945312,149.81707763671875,149.82089233398438,149.82470703125,149.82850646972656,149.8323211669922,149.8361358642578,149.83993530273438,149.84375,149.84756469726562,149.85137939453125,149.85519409179688,149.85899353027344,149.86280822753906,149.8666229248047,149.87042236328125,149.87423706054688,149.8780517578125,149.88186645507812,149.88568115234375,149.8894805908203,149.89329528808594,149.89710998535156,149.90090942382812,149.90472412109375,149.90853881835938,149.912353515625,149.91616821289062,149.9199676513672,149.9237823486328,149.92759704589844,149.931396484375,149.93521118164062,149.93902587890625,149.94284057617188,149.9466552734375,149.95045471191406,149.9542694091797,149.9580841064453,149.96188354492188,149.9656982421875,149.96951293945312,149.97332763671875,149.97714233398438,149.98094177246094,149.98475646972656,149.9885711669922,149.99237060546875,149.99618530273438,150]}"
"3100","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,4],""bins"":[149.4569091796875,149.46539306640625,149.473876953125,149.48236083984375,149.4908447265625,149.4993438720703,149.50782775878906,149.5163116455078,149.52479553222656,149.5332794189453,149.54176330566406,149.5502471923828,149.55874633789062,149.56723022460938,149.57571411132812,149.58419799804688,149.59268188476562,149.60116577148438,149.60964965820312,149.61813354492188,149.62661743164062,149.63511657714844,149.6436004638672,149.65208435058594,149.6605682373047,149.66905212402344,149.6775360107422,149.68601989746094,149.69451904296875,149.7030029296875,149.71148681640625,149.719970703125,149.72845458984375,149.7369384765625,149.74542236328125,149.75390625,149.76239013671875,149.77088928222656,149.7793731689453,149.78785705566406,149.7963409423828,149.80482482910156,149.8133087158203,149.82179260253906,149.83029174804688,149.83877563476562,149.84725952148438,149.85574340820312,149.86422729492188,149.87271118164062,149.88119506835938,149.88967895507812,149.89816284179688,149.9066619873047,149.91514587402344,149.9236297607422,149.93211364746094,149.9405975341797,149.94908142089844,149.9575653076172,149.966064453125,149.97454833984375,149.9830322265625,149.99151611328125,150]}"
"3200","{""bins"":[149.8720245361328,149.8740234375,149.8760223388672,149.87802124023438,149.88002014160156,149.88201904296875,149.88401794433594,149.88601684570312,149.8880157470703,149.8900146484375,149.8920135498047,149.89402770996094,149.89602661132812,149.8980255126953,149.9000244140625,149.9020233154297,149.90402221679688,149.90602111816406,149.90802001953125,149.91001892089844,149.91201782226562,149.9140167236328,149.916015625,149.9180145263672,149.92001342773438,149.92201232910156,149.92401123046875,149.92601013183594,149.92800903320312,149.9300079345703,149.9320068359375,149.9340057373047,149.93600463867188,149.93801879882812,149.9400177001953,149.9420166015625,149.9440155029297,149.94601440429688,149.94801330566406,149.95001220703125,149.95201110839844,149.95401000976562,149.9560089111328,149.9580078125,149.9600067138672,149.96200561523438,149.96400451660156,149.96600341796875,149.96800231933594,149.97000122070312,149.9720001220703,149.9739990234375,149.9759979248047,149.97799682617188,149.98001098632812,149.9820098876953,149.9840087890625,149.9860076904297,149.98800659179688,149.99000549316406,149.99200439453125,149.99400329589844,149.99600219726562,149.9980010986328,150],""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3]}"
"3300","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,3],""bins"":[149.5899658203125,149.59637451171875,149.602783203125,149.60919189453125,149.6156005859375,149.6219940185547,149.62840270996094,149.6348114013672,149.64122009277344,149.6476287841797,149.65403747558594,149.6604461669922,149.66683959960938,149.67324829101562,149.67965698242188,149.68606567382812,149.69247436523438,149.69888305664062,149.70529174804688,149.71170043945312,149.71810913085938,149.72450256347656,149.7309112548828,149.73731994628906,149.7437286376953,149.75013732910156,149.7565460205078,149.76295471191406,149.76934814453125,149.7757568359375,149.78216552734375,149.78857421875,149.79498291015625,149.8013916015625,149.80780029296875,149.814208984375,149.82061767578125,149.82701110839844,149.8334197998047,149.83982849121094,149.8462371826172,149.85264587402344,149.8590545654297,149.86546325683594,149.87185668945312,149.87826538085938,149.88467407226562,149.89108276367188,149.89749145507812,149.90390014648438,149.91030883789062,149.91671752929688,149.92312622070312,149.9295196533203,149.93592834472656,149.9423370361328,149.94874572753906,149.9551544189453,149.96156311035156,149.9679718017578,149.974365234375,149.98077392578125,149.9871826171875,149.99359130859375,150]}"
"3400","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,3],""bins"":[149.78488159179688,149.78823852539062,149.79161071777344,149.7949676513672,149.79832458496094,149.8016815185547,149.8050537109375,149.80841064453125,149.811767578125,149.8151397705078,149.81849670410156,149.8218536376953,149.82521057128906,149.82858276367188,149.83193969726562,149.83529663085938,149.83865356445312,149.84202575683594,149.8453826904297,149.84873962402344,149.85211181640625,149.85546875,149.85882568359375,149.8621826171875,149.8655548095703,149.86891174316406,149.8722686767578,149.87564086914062,149.87899780273438,149.88235473632812,149.88571166992188,149.8890838623047,149.89244079589844,149.8957977294922,149.899169921875,149.90252685546875,149.9058837890625,149.90924072265625,149.91261291503906,149.9159698486328,149.91932678222656,149.92269897460938,149.92605590820312,149.92941284179688,149.93276977539062,149.93614196777344,149.9394989013672,149.94285583496094,149.94622802734375,149.9495849609375,149.95294189453125,149.956298828125,149.9596710205078,149.96302795410156,149.9663848876953,149.96974182128906,149.97311401367188,149.97647094726562,149.97982788085938,149.9832000732422,149.98655700683594,149.9899139404297,149.99327087402344,149.99664306640625,150]}"
"3500","{""bins"":[149.75067138671875,149.7545623779297,149.7584686279297,149.76235961914062,149.76625061035156,149.77015686035156,149.7740478515625,149.77793884277344,149.78182983398438,149.78573608398438,149.7896270751953,149.79351806640625,149.79742431640625,149.8013153076172,149.80520629882812,149.80911254882812,149.81300354003906,149.81689453125,149.82080078125,149.82469177246094,149.82858276367188,149.83248901367188,149.8363800048828,149.84027099609375,149.84417724609375,149.8480682373047,149.85195922851562,149.85585021972656,149.85975646972656,149.8636474609375,149.86753845214844,149.87144470214844,149.87533569335938,149.8792266845703,149.8831329345703,149.88702392578125,149.8909149169922,149.8948211669922,149.89871215820312,149.90260314941406,149.906494140625,149.910400390625,149.91429138183594,149.91818237304688,149.92208862304688,149.9259796142578,149.92987060546875,149.93377685546875,149.9376678466797,149.94155883789062,149.94546508789062,149.94935607910156,149.9532470703125,149.9571533203125,149.96104431152344,149.96493530273438,149.96884155273438,149.9727325439453,149.97662353515625,149.9805145263672,149.9844207763672,149.98831176757812,149.99220275878906,149.99610900878906,150],""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,3]}"
"3600","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,3],""bins"":[149.89866638183594,149.90025329589844,149.90184020996094,149.90341186523438,149.90499877929688,149.90658569335938,149.90817260742188,149.9097442626953,149.9113311767578,149.9129180908203,149.9145050048828,149.91607666015625,149.91766357421875,149.91925048828125,149.92083740234375,149.9224090576172,149.9239959716797,149.9255828857422,149.9271697998047,149.9287567138672,149.93032836914062,149.93191528320312,149.93350219726562,149.93508911132812,149.93666076660156,149.93824768066406,149.93983459472656,149.94142150878906,149.9429931640625,149.944580078125,149.9461669921875,149.94775390625,149.9493408203125,149.95091247558594,149.95249938964844,149.95408630371094,149.95567321777344,149.95724487304688,149.95883178710938,149.96041870117188,149.96200561523438,149.9635772705078,149.9651641845703,149.9667510986328,149.9683380126953,149.96990966796875,149.97149658203125,149.97308349609375,149.97467041015625,149.97625732421875,149.9778289794922,149.9794158935547,149.9810028076172,149.9825897216797,149.98416137695312,149.98574829101562,149.98733520507812,149.98892211914062,149.99049377441406,149.99208068847656,149.99366760253906,149.99525451660156,149.996826171875,149.9984130859375,150]}"
"3700","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,3],""bins"":[149.76803588867188,149.77166748046875,149.77528381347656,149.77891540527344,149.78253173828125,149.78616333007812,149.78977966308594,149.7934112548828,149.79702758789062,149.8006591796875,149.8042755126953,149.8079071044922,149.8115234375,149.81515502929688,149.8187713623047,149.82240295410156,149.82601928710938,149.82965087890625,149.83328247070312,149.83689880371094,149.8405303955078,149.84414672851562,149.8477783203125,149.8513946533203,149.8550262451172,149.858642578125,149.86227416992188,149.8658905029297,149.86952209472656,149.87313842773438,149.87677001953125,149.88038635253906,149.88401794433594,149.8876495361328,149.89126586914062,149.8948974609375,149.8985137939453,149.9021453857422,149.90576171875,149.90939331054688,149.9130096435547,149.91664123535156,149.92025756835938,149.92388916015625,149.92750549316406,149.93113708496094,149.93475341796875,149.93838500976562,149.9420166015625,149.9456329345703,149.9492645263672,149.952880859375,149.95651245117188,149.9601287841797,149.96376037597656,149.96737670898438,149.97100830078125,149.97462463378906,149.97825622558594,149.98187255859375,149.98550415039062,149.98912048339844,149.9927520751953,149.99636840820312,150]}"
"3800","{""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,2,0,1,0,0,1,0,0,0,3],""bins"":[149.3803253173828,149.3900146484375,149.39968872070312,149.4093780517578,149.41905212402344,149.42874145507812,149.43841552734375,149.44810485839844,149.45777893066406,149.46746826171875,149.47714233398438,149.48683166503906,149.49652099609375,149.50619506835938,149.51588439941406,149.5255584716797,149.53524780273438,149.544921875,149.5546112060547,149.5642852783203,149.573974609375,149.58364868164062,149.5933380126953,149.60302734375,149.61270141601562,149.6223907470703,149.63206481933594,149.64175415039062,149.65142822265625,149.66111755371094,149.67079162597656,149.68048095703125,149.69015502929688,149.69984436035156,149.70953369140625,149.71920776367188,149.72889709472656,149.7385711669922,149.74826049804688,149.7579345703125,149.7676239013672,149.7772979736328,149.7869873046875,149.7966766357422,149.8063507080078,149.8160400390625,149.82571411132812,149.8354034423828,149.84507751464844,149.85476684570312,149.86444091796875,149.87413024902344,149.88380432128906,149.89349365234375,149.90318298339844,149.91285705566406,149.92254638671875,149.93222045898438,149.94190979003906,149.9515838623047,149.96127319335938,149.970947265625,149.9806365966797,149.9903106689453,150],""_type"":""histogram""}"
"3900","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,3],""bins"":[149.67698669433594,149.68203735351562,149.6870880126953,149.69212341308594,149.69717407226562,149.7022247314453,149.707275390625,149.71231079101562,149.7173614501953,149.722412109375,149.7274627685547,149.7324981689453,149.737548828125,149.7425994873047,149.74765014648438,149.752685546875,149.7577362060547,149.76278686523438,149.76783752441406,149.77288818359375,149.77792358398438,149.78297424316406,149.78802490234375,149.79307556152344,149.79811096191406,149.80316162109375,149.80821228027344,149.81326293945312,149.81829833984375,149.82334899902344,149.82839965820312,149.8334503173828,149.8385009765625,149.84353637695312,149.8485870361328,149.8536376953125,149.8586883544922,149.8637237548828,149.8687744140625,149.8738250732422,149.87887573242188,149.8839111328125,149.8889617919922,149.89401245117188,149.89906311035156,149.9040985107422,149.90914916992188,149.91419982910156,149.91925048828125,149.92430114746094,149.92933654785156,149.93438720703125,149.93943786621094,149.94448852539062,149.94952392578125,149.95457458496094,149.95962524414062,149.9646759033203,149.96971130371094,149.97476196289062,149.9798126220703,149.98486328125,149.98989868164062,149.9949493408203,150]}"
"4000","{""_type"":""histogram"",""values"":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,0,0,0,3],""bins"":[149.80003356933594,149.80316162109375,149.80628967285156,149.8094024658203,149.81253051757812,149.81565856933594,149.81878662109375,149.8218994140625,149.8250274658203,149.82815551757812,149.83128356933594,149.8343963623047,149.8375244140625,149.8406524658203,149.84378051757812,149.84689331054688,149.8500213623047,149.8531494140625,149.8562774658203,149.85940551757812,149.86251831054688,149.8656463623047,149.8687744140625,149.8719024658203,149.87501525878906,149.87814331054688,149.8812713623047,149.8843994140625,149.88751220703125,149.89064025878906,149.89376831054688,149.8968963623047,149.9000244140625,149.90313720703125,149.90626525878906,149.90939331054688,149.9125213623047,149.91563415527344,149.91876220703125,149.92189025878906,149.92501831054688,149.92813110351562,149.93125915527344,149.93438720703125,149.93751525878906,149.9406280517578,149.94375610351562,149.94688415527344,149.95001220703125,149.95314025878906,149.9562530517578,149.95938110351562,149.96250915527344,149.96563720703125,149.96875,149.9718780517578,149.97500610351562,149.97813415527344,149.9812469482422,149.984375,149.9875030517578,149.99063110351562,149.99374389648438,149.9968719482422,150]}"
1 Step giddy-deluge-6 - distributions/prices
2 100 {"values":[2,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1],"_type":"histogram","bins":[15.76888656616211,17.813893377780914,19.85890018939972,21.903907001018524,23.94891381263733,25.993920624256134,28.03892743587494,30.083934247493744,32.12894105911255,34.173947870731354,36.21895468235016,38.263961493968964,40.30896830558777,42.35397511720657,44.39898192882538,46.44398874044418,48.48899555206299,50.53400236368179,52.5790091753006,54.6240159869194,56.66902279853821,58.71402961015701,60.75903642177582,62.80404323339462,64.84905004501343,66.89405685663223,68.93906366825104,70.98407047986984,73.02907729148865,75.07408410310745,77.11909091472626,79.16409772634506,81.20910453796387,83.25411134958267,85.29911816120148,87.34412497282028,89.38913178443909,91.43413859605789,93.4791454076767,95.5241522192955,97.5691590309143,99.61416584253311,101.65917265415192,103.70417946577072,105.74918627738953,107.79419308900833,109.83919990062714,111.88420671224594,113.92921352386475,115.97422033548355,118.01922714710236,120.06423395872116,122.10924077033997,124.15424758195877,126.19925439357758,128.24426120519638,130.28926801681519,132.334274828434,134.3792816400528,136.4242884516716,138.4692952632904,140.5143020749092,142.55930888652802,144.60431569814682,146.64932250976562]}
3 200 {"_type":"histogram","values":[1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3],"bins":[10.439504623413086,12.620137363672256,14.800770103931427,16.981402844190598,19.162035584449768,21.34266832470894,23.52330106496811,25.70393380522728,27.88456654548645,30.06519928574562,32.24583202600479,34.42646476626396,36.60709750652313,38.7877302467823,40.96836298704147,43.148995727300644,45.329628467559814,47.510261207818985,49.690893948078156,51.871526688337326,54.0521594285965,56.23279216885567,58.41342490911484,60.59405764937401,62.77469038963318,64.95532312989235,67.13595587015152,69.31658861041069,71.49722135066986,73.67785409092903,75.8584868311882,78.03911957144737,80.21975231170654,82.40038505196571,84.58101779222488,86.76165053248405,88.94228327274323,91.1229160130024,93.30354875326157,95.48418149352074,97.66481423377991,99.84544697403908,102.02607971429825,104.20671245455742,106.38734519481659,108.56797793507576,110.74861067533493,112.9292434155941,115.10987615585327,117.29050889611244,119.47114163637161,121.65177437663078,123.83240711688995,126.01303985714912,128.1936725974083,130.37430533766747,132.55493807792664,134.7355708181858,136.91620355844498,139.09683629870415,141.27746903896332,143.4581017792225,145.63873451948166,147.81936725974083,150]}
4 300 {"bins":[92.91828918457031,93.81018829345703,94.70209503173828,95.593994140625,96.48589324951172,97.37779998779297,98.26969909667969,99.1615982055664,100.05350494384766,100.94540405273438,101.83731079101562,102.72920989990234,103.62110900878906,104.51301574707031,105.40491485595703,106.29681396484375,107.188720703125,108.08061981201172,108.97251892089844,109.86442565917969,110.7563247680664,111.64822387695312,112.54013061523438,113.4320297241211,114.32392883300781,115.21583557128906,116.10773468017578,116.9996337890625,117.89154052734375,118.78343963623047,119.67533874511719,120.56724548339844,121.45914459228516,122.35104370117188,123.24295043945312,124.13484954833984,125.02674865722656,125.91865539550781,126.81055450439453,127.70245361328125,128.5943603515625,129.48626708984375,130.37815856933594,131.2700653076172,132.16195678710938,133.05386352539062,133.94577026367188,134.83767700195312,135.7295684814453,136.62147521972656,137.51336669921875,138.4052734375,139.29718017578125,140.1890869140625,141.0809783935547,141.97288513183594,142.86477661132812,143.75668334960938,144.64859008789062,145.54049682617188,146.43238830566406,147.3242950439453,148.21620178222656,149.10809326171875,150],"_type":"histogram","values":[1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,1,0,3]}
5 400 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,3,0,0,0,3],"bins":[141.8555450439453,141.98280334472656,142.1100616455078,142.23731994628906,142.3645782470703,142.49183654785156,142.6190948486328,142.746337890625,142.87359619140625,143.0008544921875,143.12811279296875,143.25537109375,143.38262939453125,143.5098876953125,143.63714599609375,143.764404296875,143.89166259765625,144.0189208984375,144.14617919921875,144.2734375,144.4006805419922,144.52793884277344,144.6551971435547,144.78245544433594,144.9097137451172,145.03697204589844,145.1642303466797,145.29148864746094,145.4187469482422,145.54600524902344,145.6732635498047,145.80052185058594,145.92776489257812,146.05502319335938,146.18228149414062,146.30953979492188,146.43679809570312,146.56405639648438,146.69131469726562,146.81857299804688,146.94583129882812,147.07308959960938,147.20034790039062,147.32760620117188,147.45486450195312,147.58212280273438,147.70936584472656,147.8366241455078,147.96388244628906,148.0911407470703,148.21839904785156,148.3456573486328,148.47291564941406,148.6001739501953,148.72743225097656,148.8546905517578,148.98194885253906,149.1092071533203,149.2364501953125,149.36370849609375,149.490966796875,149.61822509765625,149.7454833984375,149.87274169921875,150],"_type":"histogram"}
6 500 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,3],"bins":[142.30267333984375,142.42294311523438,142.543212890625,142.66348266601562,142.78375244140625,142.90402221679688,143.0242919921875,143.14456176757812,143.26483154296875,143.38511657714844,143.50538635253906,143.6256561279297,143.7459259033203,143.86619567871094,143.98646545410156,144.1067352294922,144.2270050048828,144.34727478027344,144.46754455566406,144.5878143310547,144.7080841064453,144.82835388183594,144.94862365722656,145.0688934326172,145.18917846679688,145.3094482421875,145.42971801757812,145.54998779296875,145.67025756835938,145.79052734375,145.91079711914062,146.03106689453125,146.15133666992188,146.2716064453125,146.39187622070312,146.51214599609375,146.63241577148438,146.752685546875,146.87295532226562,146.99322509765625,147.11349487304688,147.23377990722656,147.3540496826172,147.4743194580078,147.59458923339844,147.71485900878906,147.8351287841797,147.9553985595703,148.07566833496094,148.19593811035156,148.3162078857422,148.4364776611328,148.55674743652344,148.67701721191406,148.7972869873047,148.9175567626953,149.037841796875,149.15811157226562,149.27838134765625,149.39865112304688,149.5189208984375,149.63919067382812,149.75946044921875,149.87973022460938,150]}
7 600 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,1,0,1,0,0,0,0,3],"bins":[143.02142333984375,143.13046264648438,143.239501953125,143.34854125976562,143.45758056640625,143.56661987304688,143.6756591796875,143.78469848632812,143.89373779296875,144.00279235839844,144.11183166503906,144.2208709716797,144.3299102783203,144.43894958496094,144.54798889160156,144.6570281982422,144.7660675048828,144.87510681152344,144.98414611816406,145.0931854248047,145.2022247314453,145.31126403808594,145.42030334472656,145.5293426513672,145.63839721679688,145.7474365234375,145.85647583007812,145.96551513671875,146.07455444335938,146.18359375,146.29263305664062,146.40167236328125,146.51071166992188,146.6197509765625,146.72879028320312,146.83782958984375,146.94686889648438,147.055908203125,147.16494750976562,147.27398681640625,147.38302612304688,147.49208068847656,147.6011199951172,147.7101593017578,147.81919860839844,147.92823791503906,148.0372772216797,148.1463165283203,148.25535583496094,148.36439514160156,148.4734344482422,148.5824737548828,148.69151306152344,148.80055236816406,148.9095916748047,149.0186309814453,149.127685546875,149.23672485351562,149.34576416015625,149.45480346679688,149.5638427734375,149.67288208007812,149.78192138671875,149.89096069335938,150],"_type":"histogram"}
8 700 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,1,3],"bins":[147.6887969970703,147.72491455078125,147.76101684570312,147.79713439941406,147.833251953125,147.86935424804688,147.9054718017578,147.94158935546875,147.97769165039062,148.01380920410156,148.0499267578125,148.08602905273438,148.1221466064453,148.15826416015625,148.19436645507812,148.23048400878906,148.2666015625,148.30270385742188,148.3388214111328,148.37493896484375,148.41104125976562,148.44715881347656,148.4832763671875,148.51937866210938,148.5554962158203,148.59161376953125,148.62771606445312,148.66383361816406,148.699951171875,148.73605346679688,148.7721710205078,148.80828857421875,148.84439086914062,148.88050842285156,148.9166259765625,148.95274353027344,148.9888458251953,149.02496337890625,149.0610809326172,149.09718322753906,149.13330078125,149.16941833496094,149.2055206298828,149.24163818359375,149.2777557373047,149.31385803222656,149.3499755859375,149.38609313964844,149.4221954345703,149.45831298828125,149.4944305419922,149.53053283691406,149.566650390625,149.60276794433594,149.6388702392578,149.67498779296875,149.7111053466797,149.74720764160156,149.7833251953125,149.81944274902344,149.8555450439453,149.89166259765625,149.9277801513672,149.96388244628906,150]}
9 800 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,3],"bins":[149.21865844726562,149.23086547851562,149.24307250976562,149.25527954101562,149.26748657226562,149.27969360351562,149.2919158935547,149.3041229248047,149.3163299560547,149.3285369873047,149.3407440185547,149.3529510498047,149.3651580810547,149.3773651123047,149.3895721435547,149.4017791748047,149.41400146484375,149.42620849609375,149.43841552734375,149.45062255859375,149.46282958984375,149.47503662109375,149.48724365234375,149.49945068359375,149.51165771484375,149.52386474609375,149.53607177734375,149.5482940673828,149.5605010986328,149.5727081298828,149.5849151611328,149.5971221923828,149.6093292236328,149.6215362548828,149.6337432861328,149.6459503173828,149.6581573486328,149.6703643798828,149.68258666992188,149.69479370117188,149.70700073242188,149.71920776367188,149.73141479492188,149.74362182617188,149.75582885742188,149.76803588867188,149.78024291992188,149.79244995117188,149.80465698242188,149.81687927246094,149.82908630371094,149.84129333496094,149.85350036621094,149.86570739746094,149.87791442871094,149.89012145996094,149.90232849121094,149.91453552246094,149.92674255371094,149.93896484375,149.951171875,149.96337890625,149.9755859375,149.98779296875,150]}
10 900 {"_type":"histogram","values":[1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,3],"bins":[149.2405548095703,149.25242614746094,149.2642822265625,149.27615356445312,149.28802490234375,149.2998809814453,149.31175231933594,149.32362365722656,149.33547973632812,149.34735107421875,149.35922241210938,149.37107849121094,149.38294982910156,149.3948211669922,149.40667724609375,149.41854858398438,149.430419921875,149.44227600097656,149.4541473388672,149.4660186767578,149.47787475585938,149.48974609375,149.50161743164062,149.5134735107422,149.5253448486328,149.53721618652344,149.549072265625,149.56094360351562,149.57281494140625,149.5846710205078,149.59654235839844,149.60841369628906,149.62026977539062,149.63214111328125,149.64401245117188,149.6558837890625,149.66773986816406,149.6796112060547,149.6914825439453,149.70333862304688,149.7152099609375,149.72708129882812,149.7389373779297,149.7508087158203,149.76268005371094,149.7745361328125,149.78640747070312,149.79827880859375,149.8101348876953,149.82200622558594,149.83387756347656,149.84573364257812,149.85760498046875,149.86947631835938,149.88133239746094,149.89320373535156,149.9050750732422,149.91693115234375,149.92880249023438,149.940673828125,149.95252990722656,149.9644012451172,149.9762725830078,149.98812866210938,150]}
11 1000 {"bins":[149.04977416992188,149.0646209716797,149.0794677734375,149.0943145751953,149.10916137695312,149.12400817871094,149.13885498046875,149.15370178222656,149.16854858398438,149.1833953857422,149.1982421875,149.2130889892578,149.22793579101562,149.24278259277344,149.25762939453125,149.27247619628906,149.28732299804688,149.30218505859375,149.31703186035156,149.33187866210938,149.3467254638672,149.361572265625,149.3764190673828,149.39126586914062,149.40611267089844,149.42095947265625,149.43580627441406,149.45065307617188,149.4654998779297,149.4803466796875,149.4951934814453,149.51004028320312,149.52488708496094,149.53973388671875,149.55458068847656,149.56942749023438,149.5842742919922,149.59912109375,149.6139678955078,149.62881469726562,149.64366149902344,149.65850830078125,149.67335510253906,149.68820190429688,149.7030487060547,149.7178955078125,149.7327423095703,149.74758911132812,149.762451171875,149.7772979736328,149.79214477539062,149.80699157714844,149.82183837890625,149.83668518066406,149.85153198242188,149.8663787841797,149.8812255859375,149.8960723876953,149.91091918945312,149.92576599121094,149.94061279296875,149.95545959472656,149.97030639648438,149.9851531982422,150],"_type":"histogram","values":[1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,3]}
12 1100 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,1,0,0,0,0,3],"bins":[149.34432983398438,149.3545684814453,149.3648223876953,149.37506103515625,149.38531494140625,149.3955535888672,149.40579223632812,149.41604614257812,149.42628479003906,149.43653869628906,149.44677734375,149.45701599121094,149.46726989746094,149.47750854492188,149.48776245117188,149.4980010986328,149.50823974609375,149.51849365234375,149.5287322998047,149.5389862060547,149.54922485351562,149.55947875976562,149.56971740722656,149.5799560546875,149.5902099609375,149.60044860839844,149.61070251464844,149.62094116210938,149.6311798095703,149.6414337158203,149.65167236328125,149.66192626953125,149.6721649169922,149.68240356445312,149.69265747070312,149.70289611816406,149.71315002441406,149.723388671875,149.73362731933594,149.74388122558594,149.75411987304688,149.76437377929688,149.7746124267578,149.78485107421875,149.79510498046875,149.8053436279297,149.8155975341797,149.82583618164062,149.83609008789062,149.84632873535156,149.8565673828125,149.8668212890625,149.87705993652344,149.88731384277344,149.89755249023438,149.9077911376953,149.9180450439453,149.92828369140625,149.93853759765625,149.9487762451172,149.95901489257812,149.96926879882812,149.97950744628906,149.98976135253906,150]}
13 1200 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,3],"bins":[148.93704223632812,148.95364379882812,148.9702606201172,148.9868621826172,149.00347900390625,149.02008056640625,149.0366973876953,149.0532989501953,149.06991577148438,149.08651733398438,149.10313415527344,149.11973571777344,149.1363525390625,149.1529541015625,149.16957092285156,149.18617248535156,149.20278930664062,149.21939086914062,149.23599243164062,149.2526092529297,149.2692108154297,149.28582763671875,149.30242919921875,149.3190460205078,149.3356475830078,149.35226440429688,149.36886596679688,149.38548278808594,149.40208435058594,149.418701171875,149.435302734375,149.45191955566406,149.46852111816406,149.48512268066406,149.50173950195312,149.51834106445312,149.5349578857422,149.5515594482422,149.56817626953125,149.58477783203125,149.6013946533203,149.6179962158203,149.63461303710938,149.65121459960938,149.66783142089844,149.68443298339844,149.7010498046875,149.7176513671875,149.7342529296875,149.75086975097656,149.76747131347656,149.78408813476562,149.80068969726562,149.8173065185547,149.8339080810547,149.85052490234375,149.86712646484375,149.8837432861328,149.9003448486328,149.91696166992188,149.93356323242188,149.95018005371094,149.96678161621094,149.9833984375,150],"_type":"histogram"}
14 1300 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,3],"bins":[149.0462646484375,149.06117248535156,149.07606506347656,149.09097290039062,149.10586547851562,149.1207733154297,149.13568115234375,149.15057373046875,149.1654815673828,149.18038940429688,149.19528198242188,149.21018981933594,149.22509765625,149.239990234375,149.25489807128906,149.26979064941406,149.28469848632812,149.2996063232422,149.3144989013672,149.32940673828125,149.34429931640625,149.3592071533203,149.37411499023438,149.38900756835938,149.40391540527344,149.4188232421875,149.4337158203125,149.44862365722656,149.46353149414062,149.47842407226562,149.4933319091797,149.5082244873047,149.52313232421875,149.5380401611328,149.5529327392578,149.56784057617188,149.58273315429688,149.59764099121094,149.612548828125,149.62744140625,149.64234924316406,149.65725708007812,149.67214965820312,149.6870574951172,149.70196533203125,149.71685791015625,149.7317657470703,149.7466583251953,149.76156616210938,149.77647399902344,149.79136657714844,149.8062744140625,149.8211669921875,149.83607482910156,149.85098266601562,149.86587524414062,149.8807830810547,149.89569091796875,149.91058349609375,149.9254913330078,149.94039916992188,149.95529174804688,149.97019958496094,149.98509216308594,150]}
15 1400 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,4],"bins":[148.76895141601562,148.78819274902344,148.8074188232422,148.82666015625,148.84588623046875,148.86512756347656,148.88436889648438,148.90359497070312,148.92283630371094,148.9420623779297,148.9613037109375,148.9805450439453,148.99977111816406,149.01901245117188,149.03823852539062,149.05747985839844,149.07672119140625,149.095947265625,149.1151885986328,149.13441467285156,149.15365600585938,149.17288208007812,149.19212341308594,149.21136474609375,149.2305908203125,149.2498321533203,149.26905822753906,149.28829956054688,149.3075408935547,149.32676696777344,149.34600830078125,149.365234375,149.3844757080078,149.40371704101562,149.42294311523438,149.4421844482422,149.46141052246094,149.48065185546875,149.49989318847656,149.5191192626953,149.53836059570312,149.55758666992188,149.5768280029297,149.5960693359375,149.61529541015625,149.63453674316406,149.6537628173828,149.67300415039062,149.69223022460938,149.7114715576172,149.730712890625,149.74993896484375,149.76918029785156,149.7884063720703,149.80764770507812,149.82688903808594,149.8461151123047,149.8653564453125,149.88458251953125,149.90382385253906,149.92306518554688,149.94229125976562,149.96153259277344,149.9807586669922,150]}
16 1500 {"bins":[149.55426025390625,149.56121826171875,149.5681915283203,149.5751495361328,149.58212280273438,149.58908081054688,149.59605407714844,149.60301208496094,149.6099853515625,149.616943359375,149.6239013671875,149.63087463378906,149.63783264160156,149.64480590820312,149.65176391601562,149.6587371826172,149.6656951904297,149.6726531982422,149.67962646484375,149.68658447265625,149.6935577392578,149.7005157470703,149.70748901367188,149.71444702148438,149.72140502929688,149.72837829589844,149.73533630371094,149.7423095703125,149.749267578125,149.75624084472656,149.76319885253906,149.77017211914062,149.77713012695312,149.78408813476562,149.7910614013672,149.7980194091797,149.80499267578125,149.81195068359375,149.8189239501953,149.8258819580078,149.83285522460938,149.83981323242188,149.84677124023438,149.85374450683594,149.86070251464844,149.86767578125,149.8746337890625,149.88160705566406,149.88856506347656,149.89552307128906,149.90249633789062,149.90945434570312,149.9164276123047,149.9233856201172,149.93035888671875,149.93731689453125,149.94427490234375,149.9512481689453,149.9582061767578,149.96517944335938,149.97213745117188,149.97911071777344,149.98606872558594,149.9930419921875,150],"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,3]}
17 1600 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,3],"bins":[149.17001342773438,149.1829833984375,149.19595336914062,149.20892333984375,149.22189331054688,149.23486328125,149.24781799316406,149.2607879638672,149.2737579345703,149.28672790527344,149.29969787597656,149.3126678466797,149.3256378173828,149.33860778808594,149.35157775878906,149.3645477294922,149.37750244140625,149.39047241210938,149.4034423828125,149.41641235351562,149.42938232421875,149.44235229492188,149.455322265625,149.46829223632812,149.48126220703125,149.49423217773438,149.5072021484375,149.52015686035156,149.5331268310547,149.5460968017578,149.55906677246094,149.57203674316406,149.5850067138672,149.5979766845703,149.61094665527344,149.62391662597656,149.6368865966797,149.6498565673828,149.66281127929688,149.67578125,149.68875122070312,149.70172119140625,149.71469116210938,149.7276611328125,149.74063110351562,149.75360107421875,149.76657104492188,149.779541015625,149.79251098632812,149.8054656982422,149.8184356689453,149.83140563964844,149.84437561035156,149.8573455810547,149.8703155517578,149.88328552246094,149.89625549316406,149.9092254638672,149.9221954345703,149.93515014648438,149.9481201171875,149.96109008789062,149.97406005859375,149.98703002929688,150]}
18 1700 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,3],"bins":[149.36141967773438,149.37139892578125,149.38137817382812,149.391357421875,149.40133666992188,149.41131591796875,149.42127990722656,149.43125915527344,149.4412384033203,149.4512176513672,149.46119689941406,149.47117614746094,149.4811553955078,149.4911346435547,149.50111389160156,149.51109313964844,149.52105712890625,149.53103637695312,149.541015625,149.55099487304688,149.56097412109375,149.57095336914062,149.5809326171875,149.59091186523438,149.60089111328125,149.61087036132812,149.620849609375,149.6308135986328,149.6407928466797,149.65077209472656,149.66075134277344,149.6707305908203,149.6807098388672,149.69068908691406,149.70066833496094,149.7106475830078,149.7206268310547,149.73060607910156,149.74057006835938,149.75054931640625,149.76052856445312,149.7705078125,149.78048706054688,149.79046630859375,149.80044555664062,149.8104248046875,149.82040405273438,149.83038330078125,149.84036254882812,149.85032653808594,149.8603057861328,149.8702850341797,149.88026428222656,149.89024353027344,149.9002227783203,149.9102020263672,149.92018127441406,149.93016052246094,149.9401397705078,149.95010375976562,149.9600830078125,149.97006225585938,149.98004150390625,149.99002075195312,150]}
19 1800 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,3],"bins":[149.42300415039062,149.43202209472656,149.4410400390625,149.45005798339844,149.4590606689453,149.46807861328125,149.4770965576172,149.48611450195312,149.49513244628906,149.504150390625,149.51315307617188,149.5221710205078,149.53118896484375,149.5402069091797,149.54922485351562,149.55824279785156,149.5672607421875,149.57626342773438,149.5852813720703,149.59429931640625,149.6033172607422,149.61233520507812,149.62135314941406,149.63035583496094,149.63937377929688,149.6483917236328,149.65740966796875,149.6664276123047,149.67544555664062,149.6844482421875,149.69346618652344,149.70248413085938,149.7115020751953,149.72052001953125,149.7295379638672,149.73855590820312,149.74755859375,149.75657653808594,149.76559448242188,149.7746124267578,149.78363037109375,149.7926483154297,149.80165100097656,149.8106689453125,149.81968688964844,149.82870483398438,149.8377227783203,149.84674072265625,149.85574340820312,149.86476135253906,149.873779296875,149.88279724121094,149.89181518554688,149.9008331298828,149.90985107421875,149.91885375976562,149.92787170410156,149.9368896484375,149.94590759277344,149.95492553710938,149.9639434814453,149.9729461669922,149.98196411132812,149.99098205566406,150],"_type":"histogram"}
20 1900 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,3],"bins":[148.7623748779297,148.78170776367188,148.80105590820312,148.8203887939453,148.8397216796875,148.85906982421875,148.87840270996094,148.89773559570312,148.91708374023438,148.93641662597656,148.95574951171875,148.97509765625,148.9944305419922,149.01376342773438,149.03311157226562,149.0524444580078,149.07177734375,149.09112548828125,149.11045837402344,149.12979125976562,149.14913940429688,149.16847229003906,149.18780517578125,149.2071533203125,149.2264862060547,149.24581909179688,149.26516723632812,149.2845001220703,149.3038330078125,149.32318115234375,149.34251403808594,149.36184692382812,149.38119506835938,149.40052795410156,149.41986083984375,149.43919372558594,149.4585418701172,149.47787475585938,149.49720764160156,149.5165557861328,149.535888671875,149.5552215576172,149.57456970214844,149.59390258789062,149.6132354736328,149.63258361816406,149.65191650390625,149.67124938964844,149.6905975341797,149.70993041992188,149.72926330566406,149.7486114501953,149.7679443359375,149.7872772216797,149.80662536621094,149.82595825195312,149.8452911376953,149.86463928222656,149.88397216796875,149.90330505371094,149.9226531982422,149.94198608398438,149.96131896972656,149.9806671142578,150]}
21 2000 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,2,1,0,0,0,0,1,0,0,0,0,0,1,3],"bins":[149.5408477783203,149.5480194091797,149.55519104003906,149.5623779296875,149.56954956054688,149.57672119140625,149.58389282226562,149.591064453125,149.59823608398438,149.6054229736328,149.6125946044922,149.61976623535156,149.62693786621094,149.6341094970703,149.6412811279297,149.64846801757812,149.6556396484375,149.66281127929688,149.66998291015625,149.67715454101562,149.684326171875,149.69151306152344,149.6986846923828,149.7058563232422,149.71302795410156,149.72019958496094,149.7273712158203,149.73455810546875,149.74172973632812,149.7489013671875,149.75607299804688,149.76324462890625,149.77041625976562,149.77760314941406,149.78477478027344,149.7919464111328,149.7991180419922,149.80628967285156,149.8134765625,149.82064819335938,149.82781982421875,149.83499145507812,149.8421630859375,149.84933471679688,149.8565216064453,149.8636932373047,149.87086486816406,149.87803649902344,149.8852081298828,149.8923797607422,149.89956665039062,149.90673828125,149.91390991210938,149.92108154296875,149.92825317382812,149.9354248046875,149.94261169433594,149.9497833251953,149.9569549560547,149.96412658691406,149.97129821777344,149.9784698486328,149.98565673828125,149.99282836914062,150]}
22 2100 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,3],"bins":[149.5018310546875,149.50961303710938,149.51739501953125,149.52517700195312,149.532958984375,149.54075622558594,149.5485382080078,149.5563201904297,149.56410217285156,149.57188415527344,149.5796661376953,149.5874481201172,149.59524536132812,149.60302734375,149.61080932617188,149.61859130859375,149.62637329101562,149.6341552734375,149.64193725585938,149.64971923828125,149.65750122070312,149.66529846191406,149.67308044433594,149.6808624267578,149.6886444091797,149.69642639160156,149.70420837402344,149.7119903564453,149.71978759765625,149.72756958007812,149.7353515625,149.74313354492188,149.75091552734375,149.75869750976562,149.7664794921875,149.77426147460938,149.78204345703125,149.7898406982422,149.79762268066406,149.80540466308594,149.8131866455078,149.8209686279297,149.82875061035156,149.83653259277344,149.84432983398438,149.85211181640625,149.85989379882812,149.86767578125,149.87545776367188,149.88323974609375,149.89102172851562,149.8988037109375,149.90658569335938,149.9143829345703,149.9221649169922,149.92994689941406,149.93772888183594,149.9455108642578,149.9532928466797,149.96107482910156,149.9688720703125,149.97665405273438,149.98443603515625,149.99221801757812,150]}
23 2200 {"bins":[149.558349609375,149.56524658203125,149.5721435546875,149.5790557861328,149.58595275878906,149.5928497314453,149.59976196289062,149.60665893554688,149.61355590820312,149.62045288085938,149.62734985351562,149.63426208496094,149.6411590576172,149.64805603027344,149.65496826171875,149.661865234375,149.66876220703125,149.6756591796875,149.68255615234375,149.68946838378906,149.6963653564453,149.70326232910156,149.71017456054688,149.71707153320312,149.72396850585938,149.73086547851562,149.73776245117188,149.7446746826172,149.75157165527344,149.7584686279297,149.765380859375,149.77227783203125,149.7791748046875,149.78607177734375,149.79296875,149.7998809814453,149.80677795410156,149.8136749267578,149.82058715820312,149.82748413085938,149.83438110351562,149.84127807617188,149.84817504882812,149.85508728027344,149.8619842529297,149.86888122558594,149.87579345703125,149.8826904296875,149.88958740234375,149.896484375,149.90338134765625,149.91029357910156,149.9171905517578,149.92408752441406,149.93099975585938,149.93789672851562,149.94479370117188,149.95169067382812,149.95858764648438,149.9654998779297,149.97239685058594,149.9792938232422,149.9862060546875,149.99310302734375,150],"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,2,1,1,0,0,0,3]}
24 2300 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,2,1,0,0,3],"bins":[149.1126251220703,149.12649536132812,149.14035034179688,149.1542205810547,149.1680908203125,149.18194580078125,149.19581604003906,149.20968627929688,149.22354125976562,149.23741149902344,149.25128173828125,149.26513671875,149.2790069580078,149.29287719726562,149.30673217773438,149.3206024169922,149.33447265625,149.34832763671875,149.36219787597656,149.37606811523438,149.38992309570312,149.40379333496094,149.41766357421875,149.4315185546875,149.4453887939453,149.45925903320312,149.47311401367188,149.4869842529297,149.5008544921875,149.51470947265625,149.52857971191406,149.54244995117188,149.55630493164062,149.57017517089844,149.58404541015625,149.59791564941406,149.6117706298828,149.62564086914062,149.63951110839844,149.6533660888672,149.667236328125,149.6811065673828,149.69496154785156,149.70883178710938,149.7227020263672,149.73655700683594,149.75042724609375,149.76429748535156,149.7781524658203,149.79202270507812,149.80589294433594,149.8197479248047,149.8336181640625,149.8474884033203,149.86134338378906,149.87521362304688,149.8890838623047,149.90293884277344,149.91680908203125,149.93067932128906,149.9445343017578,149.95840454101562,149.97227478027344,149.9861297607422,150]}
25 2400 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,3],"bins":[149.71124267578125,149.71575927734375,149.7202606201172,149.7247772216797,149.7292938232422,149.73379516601562,149.73831176757812,149.74282836914062,149.74734497070312,149.75184631347656,149.75636291503906,149.76087951660156,149.765380859375,149.7698974609375,149.7744140625,149.77891540527344,149.78343200683594,149.78794860839844,149.79244995117188,149.79696655273438,149.80148315429688,149.8059844970703,149.8105010986328,149.8150177001953,149.81951904296875,149.82403564453125,149.82855224609375,149.83306884765625,149.8375701904297,149.8420867919922,149.8466033935547,149.85110473632812,149.85562133789062,149.86013793945312,149.86463928222656,149.86915588378906,149.87367248535156,149.878173828125,149.8826904296875,149.88720703125,149.8917236328125,149.89622497558594,149.90074157714844,149.90525817871094,149.90975952148438,149.91427612304688,149.91879272460938,149.9232940673828,149.9278106689453,149.9323272705078,149.93682861328125,149.94134521484375,149.94586181640625,149.9503631591797,149.9548797607422,149.9593963623047,149.96389770507812,149.96841430664062,149.97293090820312,149.97744750976562,149.98194885253906,149.98646545410156,149.99098205566406,149.9954833984375,150]}
26 2500 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,0,0,3],"bins":[149.58355712890625,149.59005737304688,149.59657287597656,149.6030731201172,149.60958862304688,149.6160888671875,149.6226043701172,149.6291046142578,149.6356201171875,149.64212036132812,149.64862060546875,149.65513610839844,149.66163635253906,149.66815185546875,149.67465209960938,149.68116760253906,149.6876678466797,149.6941680908203,149.70068359375,149.70718383789062,149.7136993408203,149.72019958496094,149.72671508789062,149.73321533203125,149.73971557617188,149.74623107910156,149.7527313232422,149.75924682617188,149.7657470703125,149.7722625732422,149.7787628173828,149.7852783203125,149.79177856445312,149.79827880859375,149.80479431152344,149.81129455566406,149.81781005859375,149.82431030273438,149.83082580566406,149.8373260498047,149.84384155273438,149.850341796875,149.85684204101562,149.8633575439453,149.86985778808594,149.87637329101562,149.88287353515625,149.88938903808594,149.89588928222656,149.9023895263672,149.90890502929688,149.9154052734375,149.9219207763672,149.9284210205078,149.9349365234375,149.94143676757812,149.94793701171875,149.95445251464844,149.96095275878906,149.96746826171875,149.97396850585938,149.98048400878906,149.9869842529297,149.99349975585938,150],"_type":"histogram"}
27 2600 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,3],"bins":[149.67474365234375,149.67982482910156,149.68490600585938,149.6899871826172,149.695068359375,149.7001495361328,149.70523071289062,149.71031188964844,149.71539306640625,149.72048950195312,149.72557067871094,149.73065185546875,149.73573303222656,149.74081420898438,149.7458953857422,149.7509765625,149.7560577392578,149.76113891601562,149.76622009277344,149.77130126953125,149.77638244628906,149.78146362304688,149.7865447998047,149.7916259765625,149.79672241210938,149.8018035888672,149.806884765625,149.8119659423828,149.81704711914062,149.82212829589844,149.82720947265625,149.83229064941406,149.83737182617188,149.8424530029297,149.8475341796875,149.8526153564453,149.85769653320312,149.86277770996094,149.86785888671875,149.87294006347656,149.87802124023438,149.88311767578125,149.88819885253906,149.89328002929688,149.8983612060547,149.9034423828125,149.9085235595703,149.91360473632812,149.91868591308594,149.92376708984375,149.92884826660156,149.93392944335938,149.9390106201172,149.944091796875,149.9491729736328,149.95425415039062,149.9593505859375,149.9644317626953,149.96951293945312,149.97459411621094,149.97967529296875,149.98475646972656,149.98983764648438,149.9949188232422,150]}
28 2700 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,3],"_type":"histogram","bins":[149.65234375,149.65777587890625,149.6632080078125,149.66864013671875,149.674072265625,149.67950439453125,149.6849365234375,149.69036865234375,149.69580078125,149.70123291015625,149.7066650390625,149.71209716796875,149.717529296875,149.72296142578125,149.7283935546875,149.73382568359375,149.7392578125,149.74468994140625,149.7501220703125,149.75555419921875,149.760986328125,149.76641845703125,149.7718505859375,149.77728271484375,149.78271484375,149.78814697265625,149.7935791015625,149.79901123046875,149.804443359375,149.80987548828125,149.8153076171875,149.82073974609375,149.826171875,149.83160400390625,149.8370361328125,149.84246826171875,149.847900390625,149.85333251953125,149.8587646484375,149.86419677734375,149.86962890625,149.87506103515625,149.8804931640625,149.88592529296875,149.891357421875,149.89678955078125,149.9022216796875,149.90765380859375,149.9130859375,149.91851806640625,149.9239501953125,149.92938232421875,149.934814453125,149.94024658203125,149.9456787109375,149.95111083984375,149.95654296875,149.96197509765625,149.9674072265625,149.97283935546875,149.978271484375,149.98370361328125,149.9891357421875,149.99456787109375,150]}
29 2800 {"bins":[149.7410430908203,149.74508666992188,149.74913024902344,149.75318908691406,149.75723266601562,149.7612762451172,149.76531982421875,149.7693634033203,149.77340698242188,149.7774658203125,149.78150939941406,149.78555297851562,149.7895965576172,149.79364013671875,149.7976837158203,149.80174255371094,149.8057861328125,149.80982971191406,149.81387329101562,149.8179168701172,149.82196044921875,149.82601928710938,149.83006286621094,149.8341064453125,149.83815002441406,149.84219360351562,149.8462371826172,149.8502960205078,149.85433959960938,149.85838317871094,149.8624267578125,149.86647033691406,149.87051391601562,149.87457275390625,149.8786163330078,149.88265991210938,149.88670349121094,149.8907470703125,149.89480590820312,149.8988494873047,149.90289306640625,149.9069366455078,149.91098022460938,149.91502380371094,149.91908264160156,149.92312622070312,149.9271697998047,149.93121337890625,149.9352569580078,149.93930053710938,149.943359375,149.94740295410156,149.95144653320312,149.9554901123047,149.95953369140625,149.9635772705078,149.96763610839844,149.9716796875,149.97572326660156,149.97976684570312,149.9838104248047,149.98785400390625,149.99191284179688,149.99595642089844,150],"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,3]}
30 2900 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,3],"bins":[149.65863037109375,149.66397094726562,149.66929626464844,149.6746368408203,149.67996215820312,149.685302734375,149.6906280517578,149.6959686279297,149.7012939453125,149.70663452148438,149.71197509765625,149.71730041503906,149.72264099121094,149.72796630859375,149.73330688476562,149.73863220214844,149.7439727783203,149.7493133544922,149.754638671875,149.75997924804688,149.7653045654297,149.77064514160156,149.77597045898438,149.78131103515625,149.78665161132812,149.79197692871094,149.7973175048828,149.80264282226562,149.8079833984375,149.8133087158203,149.8186492919922,149.823974609375,149.82931518554688,149.83465576171875,149.83998107910156,149.84532165527344,149.85064697265625,149.85598754882812,149.86131286621094,149.8666534423828,149.87197875976562,149.8773193359375,149.88265991210938,149.8879852294922,149.89332580566406,149.89865112304688,149.90399169921875,149.90931701660156,149.91465759277344,149.9199981689453,149.92532348632812,149.9306640625,149.9359893798828,149.9413299560547,149.9466552734375,149.95199584960938,149.95733642578125,149.96266174316406,149.96800231933594,149.97332763671875,149.97866821289062,149.98399353027344,149.9893341064453,149.99465942382812,150]}
31 3000 {"_type":"histogram","values":[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],"bins":[149.756103515625,149.75991821289062,149.76373291015625,149.7675323486328,149.77134704589844,149.77516174316406,149.77896118164062,149.78277587890625,149.78659057617188,149.7904052734375,149.79421997070312,149.7980194091797,149.8018341064453,149.80564880371094,149.8094482421875,149.81326293945312,149.81707763671875,149.82089233398438,149.82470703125,149.82850646972656,149.8323211669922,149.8361358642578,149.83993530273438,149.84375,149.84756469726562,149.85137939453125,149.85519409179688,149.85899353027344,149.86280822753906,149.8666229248047,149.87042236328125,149.87423706054688,149.8780517578125,149.88186645507812,149.88568115234375,149.8894805908203,149.89329528808594,149.89710998535156,149.90090942382812,149.90472412109375,149.90853881835938,149.912353515625,149.91616821289062,149.9199676513672,149.9237823486328,149.92759704589844,149.931396484375,149.93521118164062,149.93902587890625,149.94284057617188,149.9466552734375,149.95045471191406,149.9542694091797,149.9580841064453,149.96188354492188,149.9656982421875,149.96951293945312,149.97332763671875,149.97714233398438,149.98094177246094,149.98475646972656,149.9885711669922,149.99237060546875,149.99618530273438,150]}
32 3100 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,4],"bins":[149.4569091796875,149.46539306640625,149.473876953125,149.48236083984375,149.4908447265625,149.4993438720703,149.50782775878906,149.5163116455078,149.52479553222656,149.5332794189453,149.54176330566406,149.5502471923828,149.55874633789062,149.56723022460938,149.57571411132812,149.58419799804688,149.59268188476562,149.60116577148438,149.60964965820312,149.61813354492188,149.62661743164062,149.63511657714844,149.6436004638672,149.65208435058594,149.6605682373047,149.66905212402344,149.6775360107422,149.68601989746094,149.69451904296875,149.7030029296875,149.71148681640625,149.719970703125,149.72845458984375,149.7369384765625,149.74542236328125,149.75390625,149.76239013671875,149.77088928222656,149.7793731689453,149.78785705566406,149.7963409423828,149.80482482910156,149.8133087158203,149.82179260253906,149.83029174804688,149.83877563476562,149.84725952148438,149.85574340820312,149.86422729492188,149.87271118164062,149.88119506835938,149.88967895507812,149.89816284179688,149.9066619873047,149.91514587402344,149.9236297607422,149.93211364746094,149.9405975341797,149.94908142089844,149.9575653076172,149.966064453125,149.97454833984375,149.9830322265625,149.99151611328125,150]}
33 3200 {"bins":[149.8720245361328,149.8740234375,149.8760223388672,149.87802124023438,149.88002014160156,149.88201904296875,149.88401794433594,149.88601684570312,149.8880157470703,149.8900146484375,149.8920135498047,149.89402770996094,149.89602661132812,149.8980255126953,149.9000244140625,149.9020233154297,149.90402221679688,149.90602111816406,149.90802001953125,149.91001892089844,149.91201782226562,149.9140167236328,149.916015625,149.9180145263672,149.92001342773438,149.92201232910156,149.92401123046875,149.92601013183594,149.92800903320312,149.9300079345703,149.9320068359375,149.9340057373047,149.93600463867188,149.93801879882812,149.9400177001953,149.9420166015625,149.9440155029297,149.94601440429688,149.94801330566406,149.95001220703125,149.95201110839844,149.95401000976562,149.9560089111328,149.9580078125,149.9600067138672,149.96200561523438,149.96400451660156,149.96600341796875,149.96800231933594,149.97000122070312,149.9720001220703,149.9739990234375,149.9759979248047,149.97799682617188,149.98001098632812,149.9820098876953,149.9840087890625,149.9860076904297,149.98800659179688,149.99000549316406,149.99200439453125,149.99400329589844,149.99600219726562,149.9980010986328,150],"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3]}
34 3300 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,3],"bins":[149.5899658203125,149.59637451171875,149.602783203125,149.60919189453125,149.6156005859375,149.6219940185547,149.62840270996094,149.6348114013672,149.64122009277344,149.6476287841797,149.65403747558594,149.6604461669922,149.66683959960938,149.67324829101562,149.67965698242188,149.68606567382812,149.69247436523438,149.69888305664062,149.70529174804688,149.71170043945312,149.71810913085938,149.72450256347656,149.7309112548828,149.73731994628906,149.7437286376953,149.75013732910156,149.7565460205078,149.76295471191406,149.76934814453125,149.7757568359375,149.78216552734375,149.78857421875,149.79498291015625,149.8013916015625,149.80780029296875,149.814208984375,149.82061767578125,149.82701110839844,149.8334197998047,149.83982849121094,149.8462371826172,149.85264587402344,149.8590545654297,149.86546325683594,149.87185668945312,149.87826538085938,149.88467407226562,149.89108276367188,149.89749145507812,149.90390014648438,149.91030883789062,149.91671752929688,149.92312622070312,149.9295196533203,149.93592834472656,149.9423370361328,149.94874572753906,149.9551544189453,149.96156311035156,149.9679718017578,149.974365234375,149.98077392578125,149.9871826171875,149.99359130859375,150]}
35 3400 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,3],"bins":[149.78488159179688,149.78823852539062,149.79161071777344,149.7949676513672,149.79832458496094,149.8016815185547,149.8050537109375,149.80841064453125,149.811767578125,149.8151397705078,149.81849670410156,149.8218536376953,149.82521057128906,149.82858276367188,149.83193969726562,149.83529663085938,149.83865356445312,149.84202575683594,149.8453826904297,149.84873962402344,149.85211181640625,149.85546875,149.85882568359375,149.8621826171875,149.8655548095703,149.86891174316406,149.8722686767578,149.87564086914062,149.87899780273438,149.88235473632812,149.88571166992188,149.8890838623047,149.89244079589844,149.8957977294922,149.899169921875,149.90252685546875,149.9058837890625,149.90924072265625,149.91261291503906,149.9159698486328,149.91932678222656,149.92269897460938,149.92605590820312,149.92941284179688,149.93276977539062,149.93614196777344,149.9394989013672,149.94285583496094,149.94622802734375,149.9495849609375,149.95294189453125,149.956298828125,149.9596710205078,149.96302795410156,149.9663848876953,149.96974182128906,149.97311401367188,149.97647094726562,149.97982788085938,149.9832000732422,149.98655700683594,149.9899139404297,149.99327087402344,149.99664306640625,150]}
36 3500 {"bins":[149.75067138671875,149.7545623779297,149.7584686279297,149.76235961914062,149.76625061035156,149.77015686035156,149.7740478515625,149.77793884277344,149.78182983398438,149.78573608398438,149.7896270751953,149.79351806640625,149.79742431640625,149.8013153076172,149.80520629882812,149.80911254882812,149.81300354003906,149.81689453125,149.82080078125,149.82469177246094,149.82858276367188,149.83248901367188,149.8363800048828,149.84027099609375,149.84417724609375,149.8480682373047,149.85195922851562,149.85585021972656,149.85975646972656,149.8636474609375,149.86753845214844,149.87144470214844,149.87533569335938,149.8792266845703,149.8831329345703,149.88702392578125,149.8909149169922,149.8948211669922,149.89871215820312,149.90260314941406,149.906494140625,149.910400390625,149.91429138183594,149.91818237304688,149.92208862304688,149.9259796142578,149.92987060546875,149.93377685546875,149.9376678466797,149.94155883789062,149.94546508789062,149.94935607910156,149.9532470703125,149.9571533203125,149.96104431152344,149.96493530273438,149.96884155273438,149.9727325439453,149.97662353515625,149.9805145263672,149.9844207763672,149.98831176757812,149.99220275878906,149.99610900878906,150],"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,3]}
37 3600 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,3],"bins":[149.89866638183594,149.90025329589844,149.90184020996094,149.90341186523438,149.90499877929688,149.90658569335938,149.90817260742188,149.9097442626953,149.9113311767578,149.9129180908203,149.9145050048828,149.91607666015625,149.91766357421875,149.91925048828125,149.92083740234375,149.9224090576172,149.9239959716797,149.9255828857422,149.9271697998047,149.9287567138672,149.93032836914062,149.93191528320312,149.93350219726562,149.93508911132812,149.93666076660156,149.93824768066406,149.93983459472656,149.94142150878906,149.9429931640625,149.944580078125,149.9461669921875,149.94775390625,149.9493408203125,149.95091247558594,149.95249938964844,149.95408630371094,149.95567321777344,149.95724487304688,149.95883178710938,149.96041870117188,149.96200561523438,149.9635772705078,149.9651641845703,149.9667510986328,149.9683380126953,149.96990966796875,149.97149658203125,149.97308349609375,149.97467041015625,149.97625732421875,149.9778289794922,149.9794158935547,149.9810028076172,149.9825897216797,149.98416137695312,149.98574829101562,149.98733520507812,149.98892211914062,149.99049377441406,149.99208068847656,149.99366760253906,149.99525451660156,149.996826171875,149.9984130859375,150]}
38 3700 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,3],"bins":[149.76803588867188,149.77166748046875,149.77528381347656,149.77891540527344,149.78253173828125,149.78616333007812,149.78977966308594,149.7934112548828,149.79702758789062,149.8006591796875,149.8042755126953,149.8079071044922,149.8115234375,149.81515502929688,149.8187713623047,149.82240295410156,149.82601928710938,149.82965087890625,149.83328247070312,149.83689880371094,149.8405303955078,149.84414672851562,149.8477783203125,149.8513946533203,149.8550262451172,149.858642578125,149.86227416992188,149.8658905029297,149.86952209472656,149.87313842773438,149.87677001953125,149.88038635253906,149.88401794433594,149.8876495361328,149.89126586914062,149.8948974609375,149.8985137939453,149.9021453857422,149.90576171875,149.90939331054688,149.9130096435547,149.91664123535156,149.92025756835938,149.92388916015625,149.92750549316406,149.93113708496094,149.93475341796875,149.93838500976562,149.9420166015625,149.9456329345703,149.9492645263672,149.952880859375,149.95651245117188,149.9601287841797,149.96376037597656,149.96737670898438,149.97100830078125,149.97462463378906,149.97825622558594,149.98187255859375,149.98550415039062,149.98912048339844,149.9927520751953,149.99636840820312,150]}
39 3800 {"values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,2,0,1,0,0,1,0,0,0,3],"bins":[149.3803253173828,149.3900146484375,149.39968872070312,149.4093780517578,149.41905212402344,149.42874145507812,149.43841552734375,149.44810485839844,149.45777893066406,149.46746826171875,149.47714233398438,149.48683166503906,149.49652099609375,149.50619506835938,149.51588439941406,149.5255584716797,149.53524780273438,149.544921875,149.5546112060547,149.5642852783203,149.573974609375,149.58364868164062,149.5933380126953,149.60302734375,149.61270141601562,149.6223907470703,149.63206481933594,149.64175415039062,149.65142822265625,149.66111755371094,149.67079162597656,149.68048095703125,149.69015502929688,149.69984436035156,149.70953369140625,149.71920776367188,149.72889709472656,149.7385711669922,149.74826049804688,149.7579345703125,149.7676239013672,149.7772979736328,149.7869873046875,149.7966766357422,149.8063507080078,149.8160400390625,149.82571411132812,149.8354034423828,149.84507751464844,149.85476684570312,149.86444091796875,149.87413024902344,149.88380432128906,149.89349365234375,149.90318298339844,149.91285705566406,149.92254638671875,149.93222045898438,149.94190979003906,149.9515838623047,149.96127319335938,149.970947265625,149.9806365966797,149.9903106689453,150],"_type":"histogram"}
40 3900 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,3],"bins":[149.67698669433594,149.68203735351562,149.6870880126953,149.69212341308594,149.69717407226562,149.7022247314453,149.707275390625,149.71231079101562,149.7173614501953,149.722412109375,149.7274627685547,149.7324981689453,149.737548828125,149.7425994873047,149.74765014648438,149.752685546875,149.7577362060547,149.76278686523438,149.76783752441406,149.77288818359375,149.77792358398438,149.78297424316406,149.78802490234375,149.79307556152344,149.79811096191406,149.80316162109375,149.80821228027344,149.81326293945312,149.81829833984375,149.82334899902344,149.82839965820312,149.8334503173828,149.8385009765625,149.84353637695312,149.8485870361328,149.8536376953125,149.8586883544922,149.8637237548828,149.8687744140625,149.8738250732422,149.87887573242188,149.8839111328125,149.8889617919922,149.89401245117188,149.89906311035156,149.9040985107422,149.90914916992188,149.91419982910156,149.91925048828125,149.92430114746094,149.92933654785156,149.93438720703125,149.93943786621094,149.94448852539062,149.94952392578125,149.95457458496094,149.95962524414062,149.9646759033203,149.96971130371094,149.97476196289062,149.9798126220703,149.98486328125,149.98989868164062,149.9949493408203,150]}
41 4000 {"_type":"histogram","values":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,0,0,0,3],"bins":[149.80003356933594,149.80316162109375,149.80628967285156,149.8094024658203,149.81253051757812,149.81565856933594,149.81878662109375,149.8218994140625,149.8250274658203,149.82815551757812,149.83128356933594,149.8343963623047,149.8375244140625,149.8406524658203,149.84378051757812,149.84689331054688,149.8500213623047,149.8531494140625,149.8562774658203,149.85940551757812,149.86251831054688,149.8656463623047,149.8687744140625,149.8719024658203,149.87501525878906,149.87814331054688,149.8812713623047,149.8843994140625,149.88751220703125,149.89064025878906,149.89376831054688,149.8968963623047,149.9000244140625,149.90313720703125,149.90626525878906,149.90939331054688,149.9125213623047,149.91563415527344,149.91876220703125,149.92189025878906,149.92501831054688,149.92813110351562,149.93125915527344,149.93438720703125,149.93751525878906,149.9406280517578,149.94375610351562,149.94688415527344,149.95001220703125,149.95314025878906,149.9562530517578,149.95938110351562,149.96250915527344,149.96563720703125,149.96875,149.9718780517578,149.97500610351562,149.97813415527344,149.9812469482422,149.984375,149.9875030517578,149.99063110351562,149.99374389648438,149.9968719482422,150]}

View File

@@ -0,0 +1,27 @@
\begin{tikzpicture}
\begin{axis}[
view={0}{90}, % Top-down view for heatmap
xlabel={Step},
ylabel={Price},
ymin=90,
colorbar,
colorbar style={
title={Density},
ylabel={},
},
colormap/viridis,
% Adjust these axis limits if necessary based on data
enlargelimits=false,
axis on top,
width=0.9\columnwidth,
height=0.5\columnwidth,
]
\addplot3[
surf,
shader=flat,
mesh/check=false % Disable check to rely on empty lines
] table [col sep=comma, x=step, y=price, z=density] {chapters/figures/supra_data.csv};
\end{axis}
\end{tikzpicture}

Some files were not shown because too many files have changed in this diff Show More