Merge pull request #52 from velocitatem/enriching-engine-simulation

Enriching engine simulation
This commit is contained in:
Daniel Alves Rösel
2026-02-25 09:21:26 +01:00
committed by GitHub
51 changed files with 8959 additions and 274 deletions

1
.gitignore vendored
View File

@@ -30,3 +30,4 @@ sim/rl/behavior_loader/*.pdf
tests/e2e/node_modules/** tests/e2e/node_modules/**
lab/case/thesis/runs*/ lab/case/thesis/runs*/
sim/case/thesis_simplified/runs*/ sim/case/thesis_simplified/runs*/
PHANTOM_web/*

View File

@@ -9,11 +9,43 @@ PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest PYTEST := $(VENV)/bin/pytest
SWEEP_ENV_FILE ?= .env.sweep
WANDB_ENTITY ?=
WANDB_PROJECT ?= phantom-pricing
SWEEP_ID ?=
LOCAL_TRAIN_ARGS ?= --algo ppo --total-timesteps 50000
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
TPU_NAME ?=
TPU_ZONE ?= us-central2-b
SWEEP_ENV_LOAD = set -a; [ -f "$(SWEEP_ENV_FILE)" ] && . "$(SWEEP_ENV_FILE)" || true; set +a
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.PHONY: help .PHONY: help
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 | test.backend test.e2e test.all | web.dev | install | train | train.agent | train.bootstrap | train.tpu.pod | stats.lines"
@echo "docker.train.publish"
@echo ""
@echo "Local wandb run:"
@echo " make train LOCAL_TRAIN_ARGS='--algo ppo --total-timesteps 50000'"
@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): $(BUILDDIR):
mkdir -p paper/$(BUILDDIR) mkdir -p paper/$(BUILDDIR)
@@ -70,6 +102,40 @@ $(VENV):
install: $(VENV) install: $(VENV)
$(PIP) install -r requirements.txt $(PIP) install -r requirements.txt
.PHONY: train
train: install
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
@$(SWEEP_ENV_LOAD); WANDB_API_KEY="$$WANDB_API_KEY" WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" \
$(PYTHON) -m engine.train $(LOCAL_TRAIN_ARGS)
.PHONY: train.agent
train.agent: install
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
@test -n "$(SWEEP_ID)" || (echo "SWEEP_ID required, e.g. SWEEP_ID=entity/project/id" && exit 1)
@$(SWEEP_ENV_LOAD); WANDB_API_KEY="$$WANDB_API_KEY" WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" \
$(PYTHON) -m engine.train --sweep-agent --sweep-id "$(SWEEP_ID)" \
$(if $(filter-out 0,$(AGENT_COUNT)),--count $(AGENT_COUNT),)
.PHONY: train.bootstrap
train.bootstrap:
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
@$(SWEEP_ENV_LOAD); test -n "$$GITHUB_TOKEN" || (echo "GITHUB_TOKEN required — set it in $(SWEEP_ENV_FILE)" && exit 1)
@test -n "$(REPO_URL)" || (echo "REPO_URL required, e.g. REPO_URL=https://github.com/org/repo.git" && exit 1)
@test -n "$(SWEEP_ID)" || (echo "SWEEP_ID required, e.g. SWEEP_ID=entity/project/id" && exit 1)
@$(SWEEP_ENV_LOAD); \
WANDB_API_KEY="$$WANDB_API_KEY" \
WANDB_ENTITY="$(WANDB_ENTITY)" \
WANDB_PROJECT="$(WANDB_PROJECT)" \
GITHUB_TOKEN="$$GITHUB_TOKEN" \
REPO_URL="$(REPO_URL)" \
BRANCH="$(BRANCH)" \
WORKDIR="$(WORKDIR)" \
SWEEP_ID="$(SWEEP_ID)" \
AGENT_COUNT="$(AGENT_COUNT)" \
AGENT_LOOP="$(AGENT_LOOP)" \
RETRY_SECONDS="$(RETRY_SECONDS)" \
bash scripts/wandb_agent_bootstrap.sh
.PHONY: stats.lines .PHONY: stats.lines
stats.lines: stats.lines:
@find . \( -path '*/node_modules' -o -path '*/.venv' -o -path '*/venv' \) -prune -o \ @find . \( -path '*/node_modules' -o -path '*/.venv' -o -path '*/venv' \) -prune -o \
@@ -86,6 +152,24 @@ wordcount:
$(SRCDIR)/chapters/05-discussion.tex \ $(SRCDIR)/chapters/05-discussion.tex \
$(SRCDIR)/chapters/06-conclusion.tex $(SRCDIR)/chapters/06-conclusion.tex
.PHONY: docker.train.publish
docker.train.publish:
docker build -f docker/Trainer.dockerfile --target gpu -t $(TRAIN_IMAGE_REF):gpu-latest .
docker push $(TRAIN_IMAGE_REF):gpu-latest
docker build -f docker/Trainer.dockerfile --target tpu -t $(TRAIN_IMAGE_REF):tpu-latest .
docker push $(TRAIN_IMAGE_REF):tpu-latest
.PHONY: train.tpu.pod
train.tpu.pod:
@test -n "$(TPU_NAME)" || (echo "TPU_NAME required, e.g. TPU_NAME=TPUlong" && exit 1)
@test -n "$(SWEEP_ID)" || (echo "SWEEP_ID required, e.g. SWEEP_ID=entity/project/id" && exit 1)
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
gcloud compute tpus tpu-vm scp scripts/tpu_pod_run.sh $(TPU_NAME):/tmp/tpu_pod_run.sh \
--zone=$(TPU_ZONE) --project=phantom-trc --worker=all
@$(SWEEP_ENV_LOAD); \
gcloud compute tpus tpu-vm ssh $(TPU_NAME) \
--zone=$(TPU_ZONE) --project=phantom-trc --worker=all \
--command="WANDB_API_KEY='$$WANDB_API_KEY' SWEEP_ID='$(SWEEP_ID)' AGENT_COUNT='$(AGENT_COUNT)' sh /tmp/tpu_pod_run.sh"
.PHONY: pdf clean watch run.webapp test count-lines all .PHONY: pdf clean watch run.webapp test count-lines all
pdf: pdf.build pdf: pdf.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

42
docker/Trainer.dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# 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
# Optional for JAX-on-GPU workflows.
ARG INSTALL_JAX_GPU=false
RUN if [ "${INSTALL_JAX_GPU}" = "true" ]; then \
pip install --no-cache-dir "jax[cuda12]==0.4.30" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html; \
fi
COPY --chmod=755 docker/trainer-agent-entrypoint.sh /usr/local/bin/trainer-agent-entrypoint
COPY engine /app/engine
ENV PYTHONPATH=/app \
XLA_PYTHON_CLIENT_PREALLOCATE=false
ENTRYPOINT ["/usr/local/bin/trainer-agent-entrypoint"]
FROM python:3.11-slim AS tpu
WORKDIR /app
COPY docker/trainer.requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
RUN pip install --no-cache-dir "jax[tpu]==0.4.30" -f https://storage.googleapis.com/jax-releases/libtpu_releases.html
COPY --chmod=755 docker/trainer-agent-entrypoint.sh /usr/local/bin/trainer-agent-entrypoint
COPY engine /app/engine
ENV PYTHONPATH=/app \
PHANTOM_USE_JAX=1 \
PHANTOM_DEFAULT_AGENT_ARGS="--jax" \
XLA_PYTHON_CLIENT_PREALLOCATE=false
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,13 @@
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
tensorflow-probability==0.24.0
flax==0.10.7
optax==0.2.7
distrax==0.1.5
orbax-checkpoint==0.11.32
chex==0.1.90

View File

@@ -1,36 +1,65 @@
from sys import platform from sys import platform
import numpy as np import numpy as np
from .lib.demand import generate_demand, estimate_demand from .lib.demand import generate_demand_for_actor, estimate_demand
from .lib.behavior import sample_behavior from .lib.behavior import sample_behavior
from logging import INFO, getLogger from logging import INFO, getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
logger.setLevel(INFO) logger.setLevel(INFO)
class MarketEngine:
"""implements separate demand distributions for humans and agents per Section 3.1.1"""
class MarketEngine(): def __init__(
def __init__(self, self,
alpha = 0.5, alpha: float,
N = 100, N: int,
demand_distribution = (50, 10), human_params: tuple,
demand_sampling_function = np.random.normal): 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.Nagents = int(N * alpha)
self.Nhumans = int(N * (1 - alpha)) self.Nhumans = int(N * (1 - alpha))
self.demand = (demand_sampling_function, demand_distribution) 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): def act(self, prices):
demand = generate_demand(prices, *self.demand) # generate separate demands d() per actor type
sample_n = lambda n, human: [sample_behavior(demand, human=human) for _ in range(n)] demand_h = generate_demand_for_actor(
human_t, agent_t = sample_n(self.Nhumans, True), sample_n(self.Nagents, False) prices,
trajectories = human_t + agent_t self.human_params,
demand_estimate = estimate_demand(trajectories) self.noise_std,
return demand_estimate distribution_method=self.demand_dist,
)
demand_a = generate_demand_for_actor(
prices,
self.agent_params,
self.noise_std,
distribution_method=self.demand_dist,
)
# sample behavior trajectories from each demand distribution
human_t = [sample_behavior(demand_h, human=True) for _ in range(self.Nhumans)]
agent_t = [sample_behavior(demand_a, human=False) 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): def measure(self):
pass pass
class PricingEngine():
def __init__(self, class PricingEngine:
def __init__(
self,
) -> None: ) -> None:
pass pass
@@ -38,29 +67,31 @@ class PricingEngine():
return np.random.uniform(low=25, high=100, size=10) return np.random.uniform(low=25, high=100, size=10)
class Limbo:
class Limbo(): def __init__(self, platform, market) -> None:
def __init__(self,
platform,
market
) -> None:
self.platform_turn = True self.platform_turn = True
self.platform = platform self.platform = platform
self.market = market self.market = market
self.output = None self.output = None
def step(self): def step(self):
# we could code golf this a little bit
if self.platform_turn: if self.platform_turn:
self.output = self.platform.act(self.output) self.output = self.platform.act(self.output)
else: else:
self.output = self.market.act(self.output) self.output = self.market.act(self.output)
print(self.output)
self.platform_turn = not self.platform_turn self.platform_turn = not self.platform_turn
return self.output
def reset(self):
self.platform_turn = True
self.output = None
if __name__ == "__main__": if __name__ == "__main__":
platform = PricingEngine() platform = PricingEngine()
market = MarketEngine() market = MarketEngine(
alpha=0.3, N=100, human_params=(50, 10), agent_params=(45, 15)
)
limbo = Limbo(platform, market) limbo = Limbo(platform, market)
for _ in range(10): for _ in range(10):
limbo.step() limbo.step()

13
engine/jax/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
"""JAX-compatible training and environment modules for PHANTOM."""
from __future__ import annotations
try:
import jax # noqa: F401
import jax.numpy as jnp # noqa: F401
JAX_AVAILABLE = True
except ImportError:
JAX_AVAILABLE = False
__all__ = ["JAX_AVAILABLE"]

49
engine/jax/checkpoint.py Normal file
View File

@@ -0,0 +1,49 @@
"""Orbax checkpoint helpers for JAX training runs."""
from __future__ import annotations
from pathlib import Path
from typing import Any
try:
import orbax.checkpoint as ocp
HAS_ORBAX = True
except ImportError:
HAS_ORBAX = False
def _require_orbax() -> None:
if not HAS_ORBAX:
raise ImportError(
"orbax-checkpoint is required for checkpoint support. "
"Install engine/jax/requirements.txt first."
)
def create_manager(directory: str | Path, max_to_keep: int = 5):
_require_orbax()
root = Path(directory)
root.mkdir(parents=True, exist_ok=True)
options = ocp.CheckpointManagerOptions(
max_to_keep=max(1, int(max_to_keep)), create=True
)
return ocp.CheckpointManager(root.as_posix(), ocp.PyTreeCheckpointer(), options)
def save(manager, *, step: int, payload: Any) -> bool:
_require_orbax()
return bool(manager.save(int(step), payload))
def latest_step(manager) -> int | None:
_require_orbax()
return manager.latest_step()
def restore(manager, *, target: Any, step: int | None = None) -> Any:
_require_orbax()
step_to_restore = manager.latest_step() if step is None else int(step)
if step_to_restore is None:
return target
return manager.restore(step_to_restore, items=target)

287
engine/jax/env.py Normal file
View File

@@ -0,0 +1,287 @@
"""JAX-native PHANTOM environment with robust contamination step."""
from __future__ import annotations
from typing import NamedTuple
try:
import jax
import jax.numpy as jnp
except ImportError as exc: # pragma: no cover
raise ImportError("engine.jax.env requires JAX") from exc
from .primitives import (
_sample_sessions_jax,
agent_probability_from_kl,
batch_kl,
compute_session_transitions,
load_transition_data,
purchase_flags,
reward_with_coi_penalty,
revenue_from_demand,
weighted_demand,
)
class EnvParams(NamedTuple):
n_products: int
n_sessions: int
max_episode_steps: int
max_session_steps: int
price_low: float
price_high: float
lambda_coi: float
info_value: float
robust_radius: float
margin_floor: float
margin_floor_patience: int
action_scales: jax.Array
alpha_nominal: float
alpha_candidates: jax.Array
human_T: jax.Array
agent_T: jax.Array
terminal_mask: jax.Array
purchase_mask: jax.Array
event_weights: jax.Array
start_idx: int
term_idx: int
class EnvState(NamedTuple):
prices: jax.Array
demand: jax.Array
step_count: jax.Array
low_margin_streak: jax.Array
last_agent_prob: jax.Array
last_alpha_adv: jax.Array
class CandidateEval(NamedTuple):
reward: jax.Array
revenue: jax.Array
demand: jax.Array
agent_prob: jax.Array
leakage: jax.Array
discount: jax.Array
n_purchases: jax.Array
n_agents: jax.Array
def make_env_params(
*,
n_products: int,
alpha: float,
n_sessions: int,
lambda_coi: float,
robust_radius: float,
robust_points: int,
info_value: float,
action_levels: int,
action_scale_low: float,
action_scale_high: float,
price_low: float,
price_high: float,
max_episode_steps: int,
max_session_steps: int = 40,
margin_floor: float = 0.05,
margin_floor_patience: int = 5,
prefer_behavior_data: bool = True,
) -> EnvParams:
transition = load_transition_data(prefer_data=prefer_behavior_data).to_jax()
if robust_radius <= 0.0 or robust_points <= 1:
alpha_candidates = jnp.asarray([float(alpha)], dtype=jnp.float32)
else:
lo = max(0.0, float(alpha) - float(robust_radius))
hi = min(1.0, float(alpha) + float(robust_radius))
alpha_candidates = jnp.linspace(lo, hi, int(robust_points), dtype=jnp.float32)
action_scales = jnp.linspace(
float(action_scale_low),
float(action_scale_high),
int(action_levels),
dtype=jnp.float32,
)
return EnvParams(
n_products=int(n_products),
n_sessions=int(n_sessions),
max_episode_steps=int(max_episode_steps),
max_session_steps=int(max_session_steps),
price_low=float(price_low),
price_high=float(price_high),
lambda_coi=float(lambda_coi),
info_value=float(info_value),
robust_radius=float(robust_radius),
margin_floor=float(margin_floor),
margin_floor_patience=int(margin_floor_patience),
action_scales=action_scales,
alpha_nominal=float(alpha),
alpha_candidates=alpha_candidates,
human_T=jnp.asarray(transition.human_T),
agent_T=jnp.asarray(transition.agent_T),
terminal_mask=jnp.asarray(transition.terminal_mask),
purchase_mask=jnp.asarray(transition.purchase_mask),
event_weights=jnp.asarray(transition.event_weights),
start_idx=int(transition.start_idx),
term_idx=int(transition.term_idx),
)
def _flatten_obs(demand: jax.Array, prices: jax.Array) -> jax.Array:
return jnp.concatenate([demand.astype(jnp.float32), prices.astype(jnp.float32)])
def _decode_action(
prices: jax.Array, action: jax.Array, params: EnvParams
) -> jax.Array:
idx = jnp.clip(action.astype(jnp.int32), 0, params.action_scales.shape[0] - 1)
scale = params.action_scales[idx]
next_prices = prices * scale
return jnp.clip(next_prices, params.price_low, params.price_high)
def _evaluate_candidate(
key: jax.Array,
alpha_candidate: jax.Array,
prices: jax.Array,
params: EnvParams,
) -> CandidateEval:
states, products, actors, lengths = _sample_sessions_jax(
key,
params.human_T,
params.agent_T,
params.terminal_mask,
params.start_idx,
params.term_idx,
alpha_candidate,
params.n_products,
params.n_sessions,
params.max_session_steps,
int(params.human_T.shape[0]),
)
session_trans = compute_session_transitions(
states, lengths, int(params.human_T.shape[0])
)
delta_h, delta_a = batch_kl(session_trans, params.human_T, params.agent_T)
agent_probs = agent_probability_from_kl(delta_h, delta_a)
agent_prob = jnp.mean(agent_probs)
demand = weighted_demand(states, products, params.n_products, params.event_weights)
revenue = revenue_from_demand(prices, demand)
reward, leakage, discount = reward_with_coi_penalty(
revenue,
agent_prob,
params.lambda_coi,
params.info_value,
)
purchases = purchase_flags(states, params.purchase_mask)
return CandidateEval(
reward=reward,
revenue=revenue,
demand=demand,
agent_prob=agent_prob,
leakage=leakage,
discount=discount,
n_purchases=jnp.sum(purchases.astype(jnp.float32)),
n_agents=jnp.sum(actors.astype(jnp.float32)),
)
def reset_env(key: jax.Array, params: EnvParams) -> tuple[jax.Array, EnvState]:
prices = jax.random.uniform(
key,
shape=(params.n_products,),
minval=params.price_low,
maxval=params.price_high,
)
demand = jnp.zeros((params.n_products,), dtype=jnp.float32)
state = EnvState(
prices=prices,
demand=demand,
step_count=jnp.asarray(0, dtype=jnp.int32),
low_margin_streak=jnp.asarray(0, dtype=jnp.int32),
last_agent_prob=jnp.asarray(params.alpha_nominal, dtype=jnp.float32),
last_alpha_adv=jnp.asarray(params.alpha_nominal, dtype=jnp.float32),
)
return _flatten_obs(demand, prices), state
def step_env(
key: jax.Array,
state: EnvState,
action: jax.Array,
params: EnvParams,
) -> tuple[jax.Array, EnvState, jax.Array, jax.Array, dict[str, jax.Array]]:
prices = _decode_action(state.prices, action, params)
n_candidates = params.alpha_candidates.shape[0]
cand_keys = jax.random.split(key, n_candidates)
evals = jax.vmap(
lambda k, a: _evaluate_candidate(k, a, prices, params),
in_axes=(0, 0),
)(cand_keys, params.alpha_candidates)
idx = jnp.argmin(evals.reward)
demand = evals.demand[idx]
reward = evals.reward[idx]
revenue = evals.revenue[idx]
agent_prob = evals.agent_prob[idx]
leakage = evals.leakage[idx]
discount = evals.discount[idx]
n_purchases = evals.n_purchases[idx]
n_agents = evals.n_agents[idx]
alpha_adv = params.alpha_candidates[idx]
step_count = state.step_count + 1
avg_price = jnp.maximum(jnp.mean(prices), 1e-6)
avg_margin = (avg_price - params.price_low) / avg_price
next_streak = jnp.where(
avg_margin < params.margin_floor, state.low_margin_streak + 1, 0
)
margin_collapsed = next_streak >= params.margin_floor_patience
done = (step_count >= params.max_episode_steps) | margin_collapsed
next_state = EnvState(
prices=prices,
demand=demand,
step_count=step_count,
low_margin_streak=next_streak,
last_agent_prob=agent_prob,
last_alpha_adv=alpha_adv,
)
obs = _flatten_obs(demand, prices)
info = {
"revenue": revenue,
"agent_prob": agent_prob,
"alpha_adv": alpha_adv,
"coi_leakage": leakage,
"coi_discount": discount,
"n_purchases": n_purchases,
"n_agents": n_agents,
"avg_margin": avg_margin,
}
return obs, next_state, reward, done, info
class PHANTOMJAXEnv:
def __init__(self, params: EnvParams):
self.params = params
def reset(self, key: jax.Array, params: EnvParams | None = None):
return reset_env(key, self.params if params is None else params)
def step(
self,
key: jax.Array,
state: EnvState,
action: jax.Array,
params: EnvParams | None = None,
):
return step_env(key, state, action, self.params if params is None else params)
def action_space_n(self, params: EnvParams | None = None) -> int:
p = self.params if params is None else params
return int(p.action_scales.shape[0])
def observation_dim(self, params: EnvParams | None = None) -> int:
p = self.params if params is None else params
return int(p.n_products * 2)

495
engine/jax/primitives.py Normal file
View File

@@ -0,0 +1,495 @@
"""JAX-compatible primitives for PHANTOM session simulation and separability."""
from __future__ import annotations
from dataclasses import dataclass
from functools import partial
from typing import Mapping, Sequence
import numpy as np
try:
import jax
import jax.numpy as jnp
JAX_AVAILABLE = True
except ImportError:
jax = None # type: ignore[assignment]
jnp = np # type: ignore[assignment]
JAX_AVAILABLE = False
STATE_START_KEYS = ("session_start", "start")
TERMINAL_EVENT_TOKENS = (
"session_end",
"end",
"purchase_complete",
"checkout_start",
"checkout",
)
PURCHASE_EVENT_TOKENS = (
"purchase_complete",
"purchase",
"checkout_start",
"checkout",
)
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",
"hover_over_title",
"hover_over_paragraph",
"hover_over_link",
"hover_over_button",
},
"nav": {
"page_view",
"view_item",
"view",
"learn_more",
"learn_more_about_item",
"view_item_page",
"session_start",
},
"filter": {
"search",
"filter_date",
"filter_price",
"sort",
"filter_for_date",
"filter_for_price",
"filter_for_amenities",
"sort_change",
},
}
DEFAULT_ACTION_WEIGHTS = {
action: CATEGORY_WEIGHTS[group]
for group, actions in ACTION_CATEGORIES.items()
for action in actions
}
@dataclass(frozen=True)
class TransitionData:
"""Dense transition kernels and per-state metadata."""
human_T: np.ndarray
agent_T: np.ndarray
terminal_mask: np.ndarray
purchase_mask: np.ndarray
event_weights: np.ndarray
event_names: tuple[str, ...]
start_idx: int
term_idx: int
def to_jax(self) -> "TransitionData":
if not JAX_AVAILABLE:
return self
return TransitionData(
human_T=jnp.asarray(self.human_T),
agent_T=jnp.asarray(self.agent_T),
terminal_mask=jnp.asarray(self.terminal_mask),
purchase_mask=jnp.asarray(self.purchase_mask),
event_weights=jnp.asarray(self.event_weights),
event_names=self.event_names,
start_idx=int(self.start_idx),
term_idx=int(self.term_idx),
)
@dataclass(frozen=True)
class SessionBatch:
states: np.ndarray
products: np.ndarray
actors: np.ndarray
lengths: np.ndarray
def _event_weight(name: str) -> float:
if name in DEFAULT_ACTION_WEIGHTS:
return float(DEFAULT_ACTION_WEIGHTS[name])
if name.startswith("hover"):
return float(CATEGORY_WEIGHTS["dwell"])
if name.startswith("filter") or name in {"search", "sort", "sort_change"}:
return float(CATEGORY_WEIGHTS["filter"])
if name.startswith("add") or name in {
"checkout",
"checkout_start",
"purchase",
"remove_item",
"purchase_complete",
}:
return float(CATEGORY_WEIGHTS["cart"])
if any(token in name for token in TERMINAL_EVENT_TOKENS):
return 0.0
return float(CATEGORY_WEIGHTS["nav"])
def _is_terminal(name: str) -> bool:
return any(token in name for token in TERMINAL_EVENT_TOKENS)
def _is_purchase(name: str) -> bool:
return any(token in name for token in PURCHASE_EVENT_TOKENS)
def _collect_events(*transitions: Mapping[str, Mapping[str, float]]) -> tuple[str, ...]:
names: set[str] = set()
for trans in transitions:
for src, dsts in trans.items():
names.add(src)
names.update(dsts.keys())
names.discard("__terminal__")
return tuple(sorted(names))
def _normalize_rows(matrix: np.ndarray, term_idx: int) -> np.ndarray:
row_sums = matrix.sum(axis=1, keepdims=True)
dead_rows = np.isclose(row_sums.squeeze(-1), 0.0)
if np.any(dead_rows):
matrix[dead_rows] = 0.0
matrix[dead_rows, term_idx] = 1.0
row_sums = matrix.sum(axis=1, keepdims=True)
return matrix / np.maximum(row_sums, 1e-8)
def _dense_from_dict(
transitions: Mapping[str, Mapping[str, float]],
event_to_idx: Mapping[str, int],
term_idx: int,
) -> np.ndarray:
n_states = len(event_to_idx)
matrix = np.zeros((n_states, n_states), dtype=np.float32)
for src, dsts in transitions.items():
i = event_to_idx.get(src)
if i is None:
continue
for dst, prob in dsts.items():
j = event_to_idx.get(dst)
if j is None:
continue
matrix[i, j] += float(prob)
return _normalize_rows(matrix, term_idx)
def compile_transition_data(
human_transitions: Mapping[str, Mapping[str, float]],
agent_transitions: Mapping[str, Mapping[str, float]],
) -> TransitionData:
event_names = _collect_events(human_transitions, agent_transitions)
if not event_names:
return fallback_transition_data()
event_names = tuple([*event_names, "__terminal__"])
term_idx = len(event_names) - 1
event_to_idx = {name: i for i, name in enumerate(event_names)}
human_T = _dense_from_dict(human_transitions, event_to_idx, term_idx)
agent_T = _dense_from_dict(agent_transitions, event_to_idx, term_idx)
terminal_mask = np.array([_is_terminal(name) for name in event_names], dtype=bool)
purchase_mask = np.array([_is_purchase(name) for name in event_names], dtype=bool)
event_weights = np.array(
[_event_weight(name) for name in event_names], dtype=np.float32
)
terminal_mask[term_idx] = True
for idx, is_term in enumerate(terminal_mask):
if not is_term:
continue
human_T[idx] = 0.0
agent_T[idx] = 0.0
human_T[idx, idx] = 1.0
agent_T[idx, idx] = 1.0
start_idx = 0
for key in STATE_START_KEYS:
if key in event_to_idx:
start_idx = int(event_to_idx[key])
break
return TransitionData(
human_T=human_T,
agent_T=agent_T,
terminal_mask=terminal_mask,
purchase_mask=purchase_mask,
event_weights=event_weights,
event_names=event_names,
start_idx=start_idx,
term_idx=term_idx,
)
def fallback_transition_data() -> TransitionData:
human = {
"session_start": {
"page_view": 0.80,
"view_item_page": 0.15,
"session_end": 0.05,
},
"page_view": {"view_item_page": 0.55, "search": 0.25, "session_end": 0.20},
"view_item_page": {
"learn_more_about_item": 0.40,
"add_item_to_cart": 0.28,
"session_end": 0.32,
},
"learn_more_about_item": {
"add_item_to_cart": 0.50,
"view_item_page": 0.30,
"session_end": 0.20,
},
"add_item_to_cart": {
"checkout_start": 0.58,
"view_item_page": 0.24,
"session_end": 0.18,
},
"checkout_start": {"purchase_complete": 0.70, "session_end": 0.30},
"purchase_complete": {"session_end": 1.0},
}
agent = {
"session_start": {
"page_view": 0.90,
"view_item_page": 0.08,
"session_end": 0.02,
},
"page_view": {"view_item_page": 0.40, "search": 0.35, "session_end": 0.25},
"view_item_page": {
"learn_more_about_item": 0.55,
"add_item_to_cart": 0.15,
"session_end": 0.30,
},
"learn_more_about_item": {
"view_item_page": 0.45,
"add_item_to_cart": 0.20,
"session_end": 0.35,
},
"add_item_to_cart": {
"checkout_start": 0.42,
"view_item_page": 0.28,
"session_end": 0.30,
},
"checkout_start": {"purchase_complete": 0.52, "session_end": 0.48},
"purchase_complete": {"session_end": 1.0},
}
return compile_transition_data(human, agent)
def load_transition_data(prefer_data: bool = True) -> TransitionData:
if not prefer_data:
return fallback_transition_data()
try:
from ..lib.behavior import get_transition_models
human_trans, agent_trans = get_transition_models()
return compile_transition_data(human_trans, agent_trans)
except Exception:
return fallback_transition_data()
if JAX_AVAILABLE:
@partial(jax.jit, static_argnums=(8, 9, 10))
def _sample_sessions_jax(
key: jax.Array,
human_T: jax.Array,
agent_T: jax.Array,
terminal_mask: jax.Array,
start_idx: int,
term_idx: int,
alpha: float,
n_products: int,
n_sessions: int,
max_steps: int,
n_states: int,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
k_actor, k_product, k_step = jax.random.split(key, 3)
start_idx_i32 = jnp.asarray(start_idx, dtype=jnp.int32)
term_idx_i32 = jnp.asarray(term_idx, dtype=jnp.int32)
actor_draw = jax.random.uniform(k_actor, (n_sessions,))
actors = (actor_draw < alpha).astype(jnp.int32)
products = jax.random.randint(
k_product, (n_sessions,), 0, n_products, dtype=jnp.int32
)
active_init = jnp.ones((n_sessions,), dtype=jnp.bool_)
state_init = jnp.full((n_sessions,), start_idx_i32, dtype=jnp.int32)
def _scan_step(carry, _):
states, active, rng = carry
rng, k = jax.random.split(rng)
probs_h = human_T[states]
probs_a = agent_T[states]
probs = jnp.where(actors[:, None] == 0, probs_h, probs_a)
next_state = jax.random.categorical(k, jnp.log(probs + 1e-10), axis=-1)
next_state = jnp.where(active, next_state, term_idx_i32)
emitted = jnp.where(active, next_state, -1)
is_terminal = terminal_mask[jnp.clip(next_state, 0, n_states - 1)]
next_active = active & (~is_terminal)
carry_states = jnp.where(next_active, next_state, term_idx_i32)
return (carry_states, next_active, rng), emitted
_, state_t = jax.lax.scan(
_scan_step, (state_init, active_init, k_step), None, length=max_steps
)
states = state_t.T
lengths = jnp.sum(states >= 0, axis=1, dtype=jnp.int32)
return states, products, actors, lengths
def sample_sessions(
key,
transition_data: TransitionData,
alpha: float,
n_products: int,
n_sessions: int,
max_steps: int,
) -> SessionBatch:
if JAX_AVAILABLE:
td = transition_data.to_jax()
states, products, actors, lengths = _sample_sessions_jax(
key,
td.human_T,
td.agent_T,
td.terminal_mask,
int(td.start_idx),
int(td.term_idx),
float(alpha),
int(n_products),
int(n_sessions),
int(max_steps),
int(td.human_T.shape[0]),
)
return SessionBatch(
states=states, products=products, actors=actors, lengths=lengths
)
rng = np.random.default_rng(int(np.asarray(key).reshape(-1)[0]))
n_states = transition_data.human_T.shape[0]
products = rng.integers(0, n_products, size=n_sessions, dtype=np.int32)
actors = (rng.random(size=n_sessions) < alpha).astype(np.int32)
states = np.full((n_sessions, max_steps), -1, dtype=np.int32)
lengths = np.zeros((n_sessions,), dtype=np.int32)
for i in range(n_sessions):
current = int(transition_data.start_idx)
mat = transition_data.agent_T if actors[i] == 1 else transition_data.human_T
for t in range(max_steps):
nxt = int(rng.choice(n_states, p=mat[current]))
states[i, t] = nxt
if transition_data.terminal_mask[nxt]:
lengths[i] = t + 1
break
current = nxt
if lengths[i] == 0:
lengths[i] = max_steps
return SessionBatch(
states=states, products=products, actors=actors, lengths=lengths
)
if JAX_AVAILABLE:
@partial(jax.jit, static_argnums=(2,))
def compute_session_transitions(states, lengths, n_states: int):
src = states[:, :-1]
dst = states[:, 1:]
time_idx = jnp.arange(src.shape[1])[None, :]
valid = (src >= 0) & (dst >= 0) & (time_idx < (lengths[:, None] - 1))
src_clip = jnp.clip(src, 0, n_states - 1)
dst_clip = jnp.clip(dst, 0, n_states - 1)
src_oh = jax.nn.one_hot(src_clip, n_states)
dst_oh = jax.nn.one_hot(dst_clip, n_states)
counts = jnp.einsum(
"nti,ntj,nt->nij", src_oh, dst_oh, valid.astype(jnp.float32)
)
row_sums = jnp.sum(counts, axis=-1, keepdims=True)
return counts / (row_sums + 1e-10)
else:
def compute_session_transitions(states, lengths, n_states: int):
trans = np.zeros((states.shape[0], n_states, n_states), dtype=np.float32)
for i in range(states.shape[0]):
for t in range(max(int(lengths[i]) - 1, 0)):
s = int(states[i, t])
d = int(states[i, t + 1])
if s >= 0 and d >= 0:
trans[i, s, d] += 1.0
row_sums = trans.sum(axis=-1, keepdims=True)
return trans / (row_sums + 1e-10)
def batch_kl(P, Q_human, Q_agent, eps: float = 1e-10):
p = P + eps
p = p / jnp.sum(p, axis=-1, keepdims=True)
qh = Q_human[None, ...] + eps
qa = Q_agent[None, ...] + eps
delta_h = jnp.sum(p * jnp.log(p / qh), axis=(1, 2))
delta_a = jnp.sum(p * jnp.log(p / qa), axis=(1, 2))
return delta_h, delta_a
if JAX_AVAILABLE:
batch_kl = jax.jit(batch_kl)
def agent_probability_from_kl(delta_h, delta_a, temperature: float = 1.0):
t = jnp.maximum(float(temperature), 1e-6)
exp_h = jnp.exp(-delta_h / t)
exp_a = jnp.exp(-delta_a / t)
return exp_a / (exp_h + exp_a + 1e-10)
def estimate_alpha_from_kl(delta_h, delta_a, beta: float = 2.0):
logits = beta * (delta_h - delta_a)
return 1.0 / (1.0 + jnp.exp(-logits))
def weighted_demand(states, products, n_products: int, event_weights):
valid = states >= 0
state_clip = jnp.clip(states, 0, event_weights.shape[0] - 1)
weights = event_weights[state_clip] * valid
per_session = jnp.sum(weights, axis=1)
demand = jnp.zeros((n_products,), dtype=jnp.float32)
demand = demand.at[products].add(per_session)
total = jnp.sum(demand)
return jnp.where(total > 0.0, (demand / total) * 100.0, demand)
if JAX_AVAILABLE:
weighted_demand = jax.jit(weighted_demand, static_argnums=(2,))
def purchase_flags(states, purchase_mask):
state_clip = jnp.clip(states, 0, purchase_mask.shape[0] - 1)
hits = purchase_mask[state_clip] & (states >= 0)
return jnp.any(hits, axis=1)
if JAX_AVAILABLE:
purchase_flags = jax.jit(purchase_flags)
def revenue_from_demand(prices, demand):
return jnp.dot(prices, demand)
if JAX_AVAILABLE:
revenue_from_demand = jax.jit(revenue_from_demand)
def reward_with_coi_penalty(
revenue, agent_prob: float, lambda_coi: float, info_value: float
):
leakage = agent_prob * info_value
discount = jnp.clip(1.0 - lambda_coi * leakage, 0.0, 1.0)
return revenue * discount, leakage, discount
if JAX_AVAILABLE:
reward_with_coi_penalty = jax.jit(reward_with_coi_penalty)

View File

@@ -0,0 +1,5 @@
flax==0.10.7
optax==0.2.7
distrax==0.1.5
orbax-checkpoint==0.11.32
chex==0.1.90

1304
engine/jax/train.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,14 @@
from .demand import generate_demand, estimate_demand from .demand import estimate_demand, estimate_weighted_demand, generate_demand_for_actor
from .behavior import sample_behavior from .behavior import sample_behavior, get_transition_models, trajectory_to_events
from .render import DashboardRenderer, style_axis from .render import DashboardRenderer, style_axis
from .wrappers import EconomicMetricsWrapper
from .callbacks import MetricsCallback, EvalMetricsCallback, CheckpointArtifactCallback
from .providers import (
ProviderBenchmark,
ProviderResult,
BenchmarkConfig,
RandomBaseline,
SurgeBaseline,
)
from .coi import compute_uplift_coi, extract_purchases, compute_agent_probability
from .discrete import EventQTable

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 pandas as pd
import numpy as np import numpy as np
from .demand import generate_demand from .demand import generate_demand_for_actor
base_dir = "/home/velocitatem/Documents/Projects/PHANTOM/experiments" base_dir = Path(__file__).parents[2] / "experiments"
human_dir, agent_dir = f"{base_dir}/collected_data/", f"{base_dir}/agents/collected_data/" human_dir = str(base_dir / "collected_data")
agent_dir = str(base_dir / "agents" / "collected_data")
_cache = {} # lazy cache for models and base pivots _cache = {} # lazy cache for models and base pivots
def _get_base_pivot(human: bool): 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: if key not in _cache:
model = BehaviorModel(human_dir) if human else AgentBehaviorModel(agent_dir) model = BehaviorModel(human_dir) if human else AgentBehaviorModel(agent_dir)
mdp = model.build_MDP() mdp = model.build_MDP()
_cache[key] = pd.DataFrame(aggregate_event_transitions(mdp)).fillna(0.0) _cache[key] = pd.DataFrame(aggregate_event_transitions(mdp)).fillna(0.0)
return _cache[key] 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): def adjust_behavior_to_condition(condition, transition_matrix):
# expand NxN transition matrix to (N*P)x(N*P) weighted by demand condition # 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) n_products = len(condition)
base_vals = transition_matrix.values 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 # 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)) expanded = np.kron(base_vals, np.outer(cond_norm, cond_norm))
@@ -29,19 +109,26 @@ 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)] 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) return pd.DataFrame(expanded, index=new_rows, columns=new_cols)
def sample_behavior(condition, human=True, max_len=40): def sample_behavior(condition, human=True, max_len=40):
base_pivot = _get_base_pivot(human) base_pivot = _get_base_pivot(human)
adjusted_transitions = adjust_behavior_to_condition(condition, base_pivot) adjusted_transitions = adjust_behavior_to_condition(condition, base_pivot)
trajectory = [np.random.choice(adjusted_transitions.index)] trajectory = [np.random.choice(adjusted_transitions.index)]
while len(trajectory) < max_len or 'checkout' in trajectory[-1]: while len(trajectory) < max_len and "checkout" not in trajectory[-1]:
probs = adjusted_transitions.loc[trajectory[-1]].values probs = np.asarray(adjusted_transitions.loc[trajectory[-1]].values, dtype=float)
sample = np.random.choice(adjusted_transitions.columns, p=probs/np.sum(probs) if np.sum(probs) > 0 else None) 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) trajectory.append(sample)
return trajectory return trajectory
if __name__ == "__main__": 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) 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) print(t)

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

@@ -0,0 +1,182 @@
"""Training callbacks for W&B/TensorBoard logging - reads from info dict."""
from pathlib import Path
from stable_baselines3.common.callbacks import BaseCallback, EvalCallback
import numpy as np
from ..wandb_checkpoint import checkpoint_artifact_name, log_checkpoint_file
try:
import wandb
HAS_WANDB = True
except ImportError:
HAS_WANDB = False
class MetricsCallback(BaseCallback):
"""Training metrics logger - reads info['economics'], logs to W&B."""
def __init__(
self, log_histograms: bool = True, log_freq: int = 100, verbose: int = 0
):
super().__init__(verbose)
self.log_histograms = log_histograms
self.log_freq = log_freq
self._episode_revenues: list[float] = []
def _on_step(self) -> bool:
if not HAS_WANDB or wandb.run is None:
return True
for info in self.locals.get("infos", []):
if "economics" not in info:
continue
econ = info["economics"]
t = self.num_timesteps
payload = {
"economics/revenue": econ["revenue"],
"economics/margin": econ["margin"],
"coi/level": econ["coi_level"],
"economics/regret": econ["regret"],
}
if "coi_mix" in econ:
payload["coi/mix"] = econ["coi_mix"]
if "coi_base" in econ:
payload["coi/base"] = econ["coi_base"]
if "coi_leakage" in econ:
payload["coi/leakage"] = econ["coi_leakage"]
if "coi_penalty" in econ:
payload["coi/penalty"] = econ["coi_penalty"]
wandb.log(payload, step=t)
self._episode_revenues.append(econ["revenue"])
# histograms at log_freq intervals
if self.log_histograms and self.num_timesteps % self.log_freq == 0:
for info in self.locals.get("infos", []):
if "prices" in info:
wandb.log(
{"distributions/prices": wandb.Histogram(info["prices"])},
step=self.num_timesteps,
)
if "demand" in info:
wandb.log(
{"distributions/demand": wandb.Histogram(info["demand"])},
step=self.num_timesteps,
)
return True
def _on_rollout_end(self) -> None:
if not HAS_WANDB or wandb.run is None or not self._episode_revenues:
return
wandb.log(
{
"episode/mean_revenue": np.mean(self._episode_revenues),
"episode/total_revenue": np.sum(self._episode_revenues),
},
step=self.num_timesteps,
)
self._episode_revenues = []
class CheckpointArtifactCallback(BaseCallback):
"""Periodic SB3 checkpoint uploader backed by W&B artifacts."""
def __init__(self, cfg: dict, interval: int = 10_000, verbose: int = 0):
super().__init__(verbose)
self.cfg = dict(cfg)
self.interval = max(1, int(interval))
self.model_dir = Path(str(self.cfg.get("model_dir", "engine/models")))
self.model_dir.mkdir(parents=True, exist_ok=True)
self._next_checkpoint = self.interval
self._last_saved_step = -1
def _artifact_name(self) -> str:
sweep_id = (
getattr(wandb.run, "sweep_id", None)
if HAS_WANDB and wandb.run is not None
else None
)
return checkpoint_artifact_name(self.cfg, backend="sb3", sweep_id=sweep_id)
def _checkpoint_file(self) -> Path:
algo = str(self.cfg.get("algo", "model"))
base = self.model_dir / f"phantom_{algo}_checkpoint"
self.model.save(str(base))
return base.with_suffix(".zip")
def _save_checkpoint(self) -> None:
if not HAS_WANDB or wandb.run is None:
return
step = int(self.num_timesteps)
if step <= self._last_saved_step:
return
checkpoint_path = self._checkpoint_file()
metadata = {
"step": step,
"algo": str(self.cfg.get("algo", "unknown")),
"sweep_id": getattr(wandb.run, "sweep_id", None),
}
saved = log_checkpoint_file(
self._artifact_name(),
file_path=checkpoint_path,
artifact_file_name=checkpoint_path.name,
metadata=metadata,
)
if saved:
self._last_saved_step = step
def _on_step(self) -> bool:
if self.num_timesteps < self._next_checkpoint:
return True
self._save_checkpoint()
while self._next_checkpoint <= self.num_timesteps:
self._next_checkpoint += self.interval
return True
def _on_training_end(self) -> None:
self._save_checkpoint()
class EvalMetricsCallback(EvalCallback):
"""Deterministic evaluation - true performance without exploration noise."""
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] = []
def _on_step(self) -> bool:
result = super()._on_step()
if not HAS_WANDB or wandb.run is None:
return result
# log eval metrics after evaluation runs
if self.n_calls % self.eval_freq == 0 and hasattr(self, "last_mean_reward"):
wandb.log(
{
"eval/mean_reward": self.last_mean_reward,
"eval/mean_revenue": np.mean(self._eval_revenues)
if self._eval_revenues
else 0,
},
step=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"])

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

@@ -0,0 +1,76 @@
import numpy as np
from typing import Dict
def compute_agent_probability(
trajectory: list, human_transitions: Dict, agent_transitions: Dict
) -> 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)
# agent_prob = exp(-kl_agent) / (exp(-kl_human) + exp(-kl_agent))
exp_h = np.exp(-kl_human)
exp_a = np.exp(-kl_agent)
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 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)): CATEGORY_WEIGHTS = {"cart": 4.0, "dwell": 2.0, "nav": 1.0, "filter": 0.5}
# assumption 1: each product has an intrinsic valuation drawn from a normal distribution centered at 50 ACTION_CATEGORIES = {
product_valuations = distribution_method(*distribution_params, size=len(prices)) "cart": {"add_item", "add_to_cart", "remove", "checkout", "purchase"},
# assumption 2: demand decreases as price increases, following a simple linear model "dwell": {"hover_title", "hover_paragraph", "hover_link"},
demand = np.maximum(0, product_valuations - prices) # demand cannot be negative "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) total = np.sum(demand)
demand = demand / total * 100 if total > 0 else demand # normalize to percentage, avoid div by zero return demand / total * 100 if total > 0 else demand
logger.info(f"Generated demand for prices {prices}: {demand} with valuations from distribution {distribution_params}")
return 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 traj in trajectories:
for event in traj: for state in traj:
if 'view_product' in event: action, product_id = _parse_event_state(state)
product_id = int(event.split('_')[-1].replace('product', '')) if product_id is None:
demand_estimate[product_id] = demand_estimate.get(product_id, 0) + 1 continue
total_views = sum(demand_estimate.values()) w = _weight_for_action(action, action_weights)
for product_id in demand_estimate: if w <= 0:
demand_estimate[product_id] = (demand_estimate[product_id] / total_views) * 100 # normalize to percentage continue
return demand_estimate 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 # Example usage
if __name__ == "__main__": if __name__ == "__main__":
np.random.seed(42) np.random.seed(42)
prices = np.array([20.0, 35.0, 50.0, 65.0]) prices = np.array([20.0, 35.0, 50.0, 65.0])
demand = generate_demand(prices) # demo actor-specific demands
print("Generated Demand:", demand) 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 from .behavior import sample_behavior
N, alphat =200, 0.1
trajectories = [] N, alpha = 200, 0.3
for _ in range(int(N*(1 - alphat))): n_h, n_a = int(N * (1 - alpha)), int(N * alpha)
trajectories.append(sample_behavior(demand, human=True)) human_t = [sample_behavior(demand_h, human=True) for _ in range(n_h)]
for _ in range(int(N*alphat)): agent_t = [sample_behavior(demand_a, human=False) for _ in range(n_a)]
trajectories.append(sample_behavior(demand, human=False)) demand_estimate = estimate_demand(human_t + agent_t)
demand_estimate = estimate_demand(trajectories)
print("Estimated Demand from Behavior:", demand_estimate) 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",
)

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

@@ -0,0 +1,77 @@
"""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))])
alpha = self.env.unwrapped.alpha
# 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"):
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]))

View File

@@ -0,0 +1,84 @@
method: random
metric:
name: sweep/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: sweep/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: sweep/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: sweep/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

@@ -1,45 +1,521 @@
from stable_baselines3 import SAC from __future__ import annotations
from stable_baselines3.common.callbacks import EvalCallback, BaseCallback
from .wrapper import PHANTOM import argparse
import json
import os
from pathlib import Path
import numpy as np
from .wandb_checkpoint import checkpoint_artifact_name, download_latest_checkpoint
try:
import wandb
HAS_WANDB = True
except ImportError:
HAS_WANDB = False
try:
from stable_baselines3 import PPO, A2C, DQN
from stable_baselines3.common.callbacks import EvalCallback
from stable_baselines3.common.monitor import Monitor
HAS_SB3 = True
except ImportError:
HAS_SB3 = False
from .jax import JAX_AVAILABLE
class RenderCallback(BaseCallback): DEFAULT_CFG = {
"""Renders environment on every step for live visualization.""" "project": "phantom-pricing",
def __init__(self, env: PHANTOM): "algo": "ppo",
super().__init__() "seed": 42,
self.env = env "total_timesteps": 50_000,
"eval_episodes": 5,
def _on_step(self) -> bool: "eval_freq": 1_000,
self.env.render() "log_freq": 100,
return True "revenue_weight": 0.01,
"n_products": 10,
"N": 100,
"alpha": 0.3,
"lambda_coi": 0.2,
"robust_radius": 0.15,
"robust_points": 5,
"info_value": 1.0,
"price_low": 10.0,
"price_high": 150.0,
"action_levels": 9,
"action_scale_low": 0.8,
"action_scale_high": 1.2,
"learning_rate": 3e-4,
"gamma": 0.99,
"buffer_size": 50_000,
"batch_size": 256,
"tau": 0.005,
"train_freq": 1,
"learning_starts": 1_000,
"target_update_interval": 1_000,
"exploration_fraction": 0.2,
"exploration_final_eps": 0.05,
"n_steps": 2_048,
"n_epochs": 10,
"gae_lambda": 0.95,
"clip_range": 0.2,
"ent_coef": 0.0,
"q_lr": 0.1,
"eps_start": 1.0,
"eps_end": 0.05,
"eps_decay": 0.9995,
"model_dir": "engine/models",
"arch": "small",
"activation": "relu",
"q_bins": 6,
"max_steps": 100,
"margin_floor": 0.05,
"margin_floor_patience": 5,
"use_jax": False,
"jax_num_envs": 16,
"jax_num_steps": 128,
"jax_num_minibatches": 4,
"jax_update_epochs": 4,
"jax_anneal_lr": True,
"checkpoint_interval": 10_000,
}
env = PHANTOM(n_products=10, alpha=0.3, render_mode="human") def _truthy(value: str | bool | None) -> bool:
eval_env = PHANTOM(n_products=10, alpha=0.3, render_mode=None) if isinstance(value, bool):
return value
if value is None:
return False
return str(value).strip().lower() in {"1", "true", "yes", "on"}
model = SAC(
"MultiInputPolicy", def _cfg(raw: dict | None = None) -> dict:
env, cfg = dict(DEFAULT_CFG)
verbose=1, if raw:
learning_rate=3e-4, cfg.update({k: v for k, v in raw.items() if v is not None})
buffer_size=50000, cfg["algo"] = str(cfg["algo"]).lower()
batch_size=256, cfg["use_jax"] = _truthy(cfg.get("use_jax")) or _truthy(
tau=0.005, os.environ.get("PHANTOM_USE_JAX")
gamma=0.99, )
return cfg
def _wandb_cfg_dict() -> dict:
return (
{k: wandb.config[k] for k in wandb.config.keys()}
if HAS_WANDB and wandb.run
else {}
) )
render_cb = RenderCallback(env)
eval_cb = EvalCallback(eval_env, eval_freq=1000, n_eval_episodes=5, verbose=1)
model.learn(total_timesteps=50000, callback=[render_cb, eval_cb]) def make_env(cfg: dict):
model.save("phantom_sac") from gymnasium.wrappers import FlattenObservation
# test trained policy from .wrapper import PHANTOM
env = PHANTOM(n_products=10, alpha=0.3, render_mode="human") from .lib.wrappers import EconomicMetricsWrapper
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"]),
info_value=float(cfg["info_value"]),
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)
env = FlattenObservation(env)
return env
def _net_arch(name) -> 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]
s = str(name).lower().strip()
if s in presets:
return presets[s]
if "x" in s:
try:
vals = [int(v) for v in s.split("x") if v]
return vals if vals else presets["small"]
except ValueError:
return presets["small"]
return presets["small"]
def _activation(name):
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: dict) -> dict:
kw = {"net_arch": _net_arch(cfg.get("arch", "small"))}
act = _activation(cfg.get("activation", "relu"))
if act is not None:
kw["activation_fn"] = act
return kw
def _action(agent, obs, deterministic: bool = True):
out = agent.predict(obs, deterministic=deterministic)
a = out[0] if isinstance(out, tuple) else out
if isinstance(a, np.ndarray) and a.size == 1:
return int(a.reshape(-1)[0])
return a
def evaluate(agent, env, episodes: int) -> dict:
rewards, revenues = [], []
for _ in range(int(episodes)):
obs, _ = env.reset() obs, _ = env.reset()
for _ in range(100): done, ep_r, ep_rev = False, 0.0, 0.0
action, _ = model.predict(obs, deterministic=True) while not done:
obs, reward, term, trunc, _ = env.step(action) obs, reward, term, trunc, info = env.step(_action(agent, obs, True))
env.render() done = term or trunc
if term or trunc: break ep_r += float(reward)
ep_rev += float(
info.get("economics", {}).get("revenue", info.get("revenue", 0.0))
)
rewards.append(ep_r)
revenues.append(ep_rev)
return {
"eval/reward": float(np.mean(rewards)),
"eval/revenue": float(np.mean(revenues)),
"eval/reward_std": float(np.std(rewards)),
"eval/revenue_std": float(np.std(revenues)),
}
def build_model(cfg: dict, env):
algo = cfg["algo"]
policy_kwargs = _policy_kwargs(cfg)
if algo == "sac":
raise ValueError("sac is not supported with the discrete core env")
if algo == "ppo":
return PPO(
"MlpPolicy",
env,
verbose=1,
policy_kwargs=policy_kwargs,
seed=int(cfg["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,
policy_kwargs=policy_kwargs,
seed=int(cfg["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,
policy_kwargs=policy_kwargs,
seed=int(cfg["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 _sb3_model_cls(algo: str):
if algo == "ppo":
return PPO
if algo == "a2c":
return A2C
if algo == "dqn":
return DQN
raise ValueError(f"unsupported algo '{algo}'")
def train_qtable(cfg: dict) -> tuple[EventQTable, dict]:
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"]),
)
eps = float(cfg["eps_start"])
obs, _ = env.reset(seed=int(cfg["seed"]))
for t in range(int(cfg["total_timesteps"])):
a, s = agent.act(obs, eps)
nxt, reward, term, trunc, info = env.step(a)
done = term or trunc
agent.update(s, a, float(reward), agent.encode(nxt), done)
eps = max(float(cfg["eps_end"]), eps * float(cfg["eps_decay"]))
if HAS_WANDB and wandb.run and (t + 1) % int(cfg["log_freq"]) == 0:
econ = info.get("economics", {})
wandb.log(
{
"train/reward": float(reward),
"train/revenue": float(econ.get("revenue", 0.0)),
"train/epsilon": float(eps),
},
step=t + 1,
)
obs = env.reset()[0] if done else nxt
metrics = evaluate(agent, eval_env, int(cfg["eval_episodes"]))
metrics["train/global_step"] = int(cfg["total_timesteps"])
env.close() env.close()
eval_env.close()
return agent, metrics
def train_sb3(cfg: dict) -> tuple[object, dict]:
if not HAS_SB3:
raise ImportError("stable-baselines3 is required for SB3 models")
from .lib.callbacks import CheckpointArtifactCallback, MetricsCallback
env = make_env(cfg)
eval_env = make_env(cfg)
env = Monitor(env)
eval_env = Monitor(eval_env)
model = build_model(cfg, env)
resume_step = 0
if HAS_WANDB and wandb.run is not None:
sweep_id = getattr(wandb.run, "sweep_id", None)
artifact_name = checkpoint_artifact_name(cfg, backend="sb3", sweep_id=sweep_id)
checkpoint_file = f"phantom_{cfg['algo']}_checkpoint.zip"
restored = download_latest_checkpoint(artifact_name, file_name=checkpoint_file)
if restored is not None:
checkpoint_path, metadata = restored
model = _sb3_model_cls(cfg["algo"]).load(
checkpoint_path.as_posix(), env=env
)
resume_step = int(metadata.get("step", getattr(model, "num_timesteps", 0)))
model.num_timesteps = max(
int(getattr(model, "num_timesteps", 0)), resume_step
)
cbs = [MetricsCallback(log_histograms=True, log_freq=int(cfg["log_freq"]))]
cbs.append(
CheckpointArtifactCallback(
cfg,
interval=int(cfg.get("checkpoint_interval", 10_000)),
)
)
cbs.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=cbs,
reset_num_timesteps=False,
)
model_path = Path(cfg["model_dir"])
model_path.mkdir(parents=True, exist_ok=True)
model.save(str(model_path / f"phantom_{cfg['algo']}"))
metrics = evaluate(model, eval_env, int(cfg["eval_episodes"]))
metrics["train/global_step"] = int(model.num_timesteps)
env.close()
eval_env.close()
return model, metrics
def train_once(cfg: dict) -> dict:
algo = cfg["algo"]
if cfg.get("use_jax"):
if not JAX_AVAILABLE:
raise ImportError(
"JAX backend requested but JAX is not installed. "
"Install engine/jax/requirements.txt and jax[tpu] for TPU runs."
)
try:
from .jax.train import train_jax
except Exception as exc: # pragma: no cover
raise ImportError(f"Failed to import JAX trainer: {exc}") from exc
_, metrics = train_jax(cfg)
elif algo == "qtable":
_, metrics = train_qtable(cfg)
else:
_, metrics = train_sb3(cfg)
metrics["sweep/score"] = float(
metrics["eval/reward"] + float(cfg["revenue_weight"]) * metrics["eval/revenue"]
)
return metrics
def run_wandb(
project: str, overrides: dict, mode: str = "online", sweep_mode: bool = False
) -> dict:
if not HAS_WANDB:
raise ImportError("wandb is required for sweep runs")
init_kwargs = {"mode": mode}
if sweep_mode:
run = wandb.init(**init_kwargs)
else:
run = wandb.init(project=project, config=overrides, **init_kwargs)
try:
cfg = _cfg(_wandb_cfg_dict())
if sweep_mode:
for k, v in overrides.items():
if k not in wandb.config:
cfg[k] = v
metrics = train_once(cfg)
step = int(metrics.get("train/global_step", cfg["total_timesteps"]))
wandb.log(metrics, step=step)
for k, v in metrics.items():
run.summary[k] = v
return metrics
finally:
if wandb.run is not None:
wandb.finish()
def run_local(overrides: dict) -> dict:
cfg = _cfg(overrides)
metrics = train_once(cfg)
print(json.dumps(metrics, indent=2))
return metrics
def main():
p = argparse.ArgumentParser(description="PHANTOM training and W&B sweeps")
p.add_argument("--project", default=DEFAULT_CFG["project"])
p.add_argument("--algo", choices=["ppo", "a2c", "dqn", "qtable"])
p.add_argument("--total-timesteps", type=int)
p.add_argument("--alpha", type=float)
p.add_argument("--n-products", type=int)
p.add_argument("--lambda-coi", type=float)
p.add_argument("--robust-radius", type=float)
p.add_argument("--robust-points", type=int)
p.add_argument("--learning-rate", type=float)
p.add_argument("--gamma", type=float)
p.add_argument("--revenue-weight", type=float)
p.add_argument("--max-steps", type=int)
p.add_argument("--margin-floor", type=float)
p.add_argument("--margin-floor-patience", type=int)
p.add_argument("--arch", type=str)
p.add_argument("--activation", type=str)
p.add_argument("--jax", action="store_true")
p.add_argument("--jax-num-envs", type=int)
p.add_argument("--jax-num-steps", type=int)
p.add_argument("--jax-num-minibatches", type=int)
p.add_argument("--jax-update-epochs", type=int)
p.add_argument("--jax-anneal-lr", type=str)
p.add_argument("--checkpoint-interval", type=int)
p.add_argument("--sweep-agent", action="store_true")
p.add_argument("--sweep-id", type=str)
p.add_argument("--count", type=int, default=0)
p.add_argument("--offline", action="store_true")
p.add_argument("--no-wandb", action="store_true")
args = p.parse_args()
overrides = {
"algo": args.algo,
"total_timesteps": args.total_timesteps,
"alpha": args.alpha,
"n_products": args.n_products,
"lambda_coi": args.lambda_coi,
"robust_radius": args.robust_radius,
"robust_points": args.robust_points,
"learning_rate": args.learning_rate,
"gamma": args.gamma,
"revenue_weight": args.revenue_weight,
"max_steps": args.max_steps,
"margin_floor": args.margin_floor,
"margin_floor_patience": args.margin_floor_patience,
"arch": args.arch,
"activation": args.activation,
"use_jax": args.jax,
"jax_num_envs": args.jax_num_envs,
"jax_num_steps": args.jax_num_steps,
"jax_num_minibatches": args.jax_num_minibatches,
"jax_update_epochs": args.jax_update_epochs,
"checkpoint_interval": args.checkpoint_interval,
"jax_anneal_lr": _truthy(args.jax_anneal_lr)
if args.jax_anneal_lr is not None
else None,
}
overrides = {k: v for k, v in overrides.items() if v is not None}
if args.sweep_agent:
if args.no_wandb:
raise ValueError("sweep agent requires wandb")
if not args.sweep_id:
raise ValueError("--sweep-id is required with --sweep-agent")
mode = "offline" if args.offline else "online"
wandb.agent(
args.sweep_id,
function=lambda: run_wandb(
args.project, overrides, mode=mode, sweep_mode=True
),
count=args.count if args.count > 0 else None,
)
return
if args.no_wandb or not HAS_WANDB:
run_local(overrides)
return
run_wandb(args.project, overrides, mode="offline" if args.offline else "online")
if __name__ == "__main__":
main()

View File

@@ -3,39 +3,108 @@ from gymnasium import spaces
import numpy as np import numpy as np
from .engine import Limbo, MarketEngine, PricingEngine from .engine import Limbo, MarketEngine, PricingEngine
from .lib.render import DashboardRenderer 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): 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"]} metadata = {"render_modes": ["human", "ansi"]}
def __init__(self, def __init__(
self,
n_products: int = 10, n_products: int = 10,
alpha: float = 0.3, alpha: float = 0.3,
N: int = 100, 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), price_bounds: tuple = (10.0, 150.0),
lambda_coi: float = 0.1, lambda_coi: float = 0.1,
render_mode: str = None): coi_window: int = 10,
robust_radius: float = 0.0,
robust_points: int = 5,
info_value: 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__() super().__init__()
self.n_products = n_products self.n_products = n_products
self.price_bounds = price_bounds self.price_bounds = price_bounds
self.lambda_coi = lambda_coi 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.render_mode = render_mode
self.alpha = alpha self.alpha = float(alpha)
self.nominal_alpha = float(alpha)
self.N = N self.N = N
self.human_params = human_params
self.market = MarketEngine(alpha=alpha, N=N) self.agent_params = agent_params
self._platform_stub = PricingEngine() self.robust_radius = max(0.0, float(robust_radius))
self._limbo = Limbo(self._platform_stub, self.market) self.robust_points = max(1, int(robust_points))
self.info_value = float(info_value)
self.action_space = spaces.Box( self.action_levels = max(2, int(action_levels))
low=price_bounds[0], high=price_bounds[1], self._action_scales = np.linspace(
shape=(n_products,), dtype=np.float32 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,
),
}
) )
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._prices = None
self._demand = None self._demand = None
@@ -44,41 +113,179 @@ class PHANTOM(gym.Env):
self._price_history = [] self._price_history = []
self._revenue_history = [] self._revenue_history = []
self._renderer = None 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
# 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: 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) 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)} return {"demand": demand_arr, "prices": self._prices.astype(np.float32)}
def _compute_reward(self, prices: np.ndarray, demand: dict) -> float: def _set_market_mix(self, alpha: float):
revenue = np.sum(prices * np.array([demand.get(i, 0.0) for i in range(self.n_products)])) alpha = float(np.clip(alpha, 0.0, 1.0))
# TODO: implement supra-competitive price punishment n_agents = int(self.N * alpha)
return float(revenue) 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))
purchases = extract_purchases(trajectories)
coi_mix = compute_uplift_coi(prices, purchases, self.baseline_prices)
# multiplicative penalty so COI term scales with revenue magnitude
coi_leakage = float(agent_prob * self.info_value)
discount = float(np.clip(1.0 - self.lambda_coi * coi_leakage, 0.0, 1.0))
coi_penalty = revenue * (1.0 - discount) # absolute penalty in revenue units
reward = revenue * discount
return reward, {
"revenue": revenue,
"coi_mix": float(coi_mix),
"coi_base": 0.0,
"coi_leakage": coi_leakage,
"coi_penalty": coi_penalty,
"coi_discount": discount,
}
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 _select_adversarial_alpha(
self, prices: np.ndarray
) -> tuple[float, dict, list, float]:
"""inner robust step: pick worst-case alpha and return its outcome directly to avoid double-sampling"""
candidates = self._alpha_candidates()
best_alpha, worst_reward = float(candidates[0]), np.inf
best_demand, best_trajectories, best_agent_prob = None, [], 0.0
for alpha in candidates:
self._set_market_mix(float(alpha))
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)
if reward < worst_reward:
worst_reward = reward
best_alpha, best_demand, best_trajectories, best_agent_prob = (
float(alpha),
demand,
trajectories,
agent_prob,
)
return best_alpha, best_demand, best_trajectories, best_agent_prob
def _record_history(self): 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._demand_history.append(demand_arr)
self._price_history.append(self._prices.copy()) self._price_history.append(self._prices.copy())
self._revenue_history.append(np.sum(self._prices * demand_arr)) self._revenue_history.append(np.sum(self._prices * demand_arr))
def reset(self, seed=None, options=None): def reset(self, seed=None, options=None):
super().reset(seed=seed) 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._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._step_count = 0
self._low_margin_streak = 0
self._demand_history, self._price_history, self._revenue_history = [], [], [] self._demand_history, self._price_history, self._revenue_history = [], [], []
self._trajectories = list(getattr(self.market, "last_trajectories", []))
self._record_history() self._record_history()
return self._get_obs(), {} return self._get_obs(), {}
def step(self, action: np.ndarray): def step(self, action):
self._prices = np.clip(action, *self.price_bounds) self._prices = self._decode_action(action)
self._demand = self.market.act(self._prices) # inner robust step returns worst-case outcome directly, no re-sampling
alpha_adv, self._demand, trajectories, agent_prob = (
self._select_adversarial_alpha(self._prices)
)
self._set_market_mix(alpha_adv)
self._platform_stub.set_prices(self._prices)
self._step_count += 1 self._step_count += 1
self._trajectories.extend(trajectories)
reward, metrics = self._compute_reward(
self._prices, self._demand, agent_prob, trajectories
)
self._record_history() self._record_history()
reward = self._compute_reward(self._prices, self._demand) # soft early termination when margin collapses for too long
terminated = self._step_count >= 100 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),
"wasserstein_radius": float(self.robust_radius),
**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: def _compute_elasticity(self) -> np.ndarray:
"""point elasticity: e = (dQ/dP) * (P/Q) via finite differences, clipped to [-5, 5]""" """point elasticity: e = (dQ/dP) * (P/Q) via finite differences, clipped to [-5, 5]"""
@@ -87,10 +294,16 @@ class PHANTOM(gym.Env):
p, q = np.array(self._price_history), np.array(self._demand_history) p, q = np.array(self._price_history), np.array(self._demand_history)
dp, dq = np.diff(p, axis=0), np.diff(q, axis=0) dp, dq = np.diff(p, axis=0), np.diff(q, axis=0)
valid = np.abs(dp) > 0.5 valid = np.abs(dp) > 0.5
with np.errstate(divide='ignore', invalid='ignore'): 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.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) 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): def render(self):
if self.render_mode == "human": if self.render_mode == "human":
@@ -98,7 +311,9 @@ class PHANTOM(gym.Env):
self._renderer = DashboardRenderer() self._renderer = DashboardRenderer()
self._renderer.render(self) self._renderer.render(self)
elif self.render_mode == "ansi": 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 return None
def close(self): def close(self):
@@ -108,11 +323,44 @@ class PHANTOM(gym.Env):
if __name__ == "__main__": if __name__ == "__main__":
env = PHANTOM(n_products=15, alpha=0.3, N=100, render_mode="human") import wandb
obs, _ = env.reset() from .lib import MetricsCallback
for step in range(100):
action = env.action_space.sample() class RandomPolicy:
obs, reward, term, trunc, info = env.step(action) """Minimal SB3-compatible random policy for baseline testing."""
env.render()
if term: break 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="phantom-pricing", 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() env.close()

View File

@@ -42,6 +42,10 @@ EOF
# Process each directory # Process each directory
echo "Concatenating code from source directories..." 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 # 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 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" add_file "$file"
@@ -53,7 +57,7 @@ find "$PROJECT_ROOT/experiments" -type d \( -name ".venv" -o -name "__pycache__"
done done
# Docker # 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" add_file "$file"
done done

View File

@@ -562,3 +562,57 @@ Volume: 21},
note = {No. 3:25-cv-09514-MMC}, 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}, 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},
}

View File

@@ -10,7 +10,7 @@
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. 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 31st 2026.}
\subsection{Motivation and Market Context} \subsection{Motivation and Market Context}
@@ -27,19 +27,20 @@ We formally define interaction data as coming from some actor which can either b
\subsection{Research Questions} \subsection{Research Questions}
This work addresses three core research questions: This dissertation is organized around one main research question and three supporting sub-questions:
\begin{enumerate} \begin{enumerate}
\item[\textbf{RQ1}] \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{Main RQ}] How can dynamic pricing systems preserve margin integrity when transaction orchestration is increasingly mediated by non-human agents?
\item[\textbf{RQ2}] \textit{Theoretical Impact}: What is the formal relationship between agent contamination levels and the erosion of pricing power in dynamic pricing systems? \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{RQ3}] \textit{Robust Mitigation}: How can pricing policies be constructed to maintain margin integrity under unknown and non-stationary levels of agent contamination? \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} \end{enumerate}
\begin{algorithm}[t] \begin{algorithm}[t]
\DontPrintSemicolon \DontPrintSemicolon
\SetKwInOut{Input}{Input} \SetKwInput{Input}{Input}
\SetKwInOut{Output}{Output} \SetKwInput{Output}{Output}
\Input{Goal $G$, Platform URL $u$, LLM $\mathcal{M}$} \Input{Goal $G$, Platform URL $u$, LLM $\mathcal{M}$}
\Output{Task completion result $r$} \Output{Task completion result $r$}

View File

@@ -50,6 +50,7 @@ Our effort to combat contamination stems from research by \textcite{hardt_strate
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. 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. 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. 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.

View File

@@ -1,5 +1,8 @@
\section{Methodology} \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. 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} \subsection{Problem Formalization}
@@ -24,6 +27,12 @@ The platform does not directly observe the true underlying demand function $d(p)
\end{equation} \end{equation}
where $\omega: \mathcal{A} \to \mathbb{R}_+$ assigns weights to actions based on their signal strength regarding willingness to pay. where $\omega: \mathcal{A} \to \mathbb{R}_+$ assigns weights to actions based on their signal strength regarding willingness to pay.
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} \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: 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} \begin{equation}
@@ -36,15 +45,18 @@ where $\alpha \in [0, 1]$ represents the contamination parameter (proportion of
\subsection{Cost of Information (COI) Framework} \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] \begin{definition}[Cost of Information]
Let $\pi(\tau)$ be a pricing policy mapping interaction histories to prices. The COI is defined as: Let $\pi(\tau)$ be a pricing policy mapping interaction histories to prices. The COI is defined as:
\begin{align} \begin{equation}
\text{COI} &= \mathbb{E}[P] - \underline{p} \\ \text{COI} = \mathbb{E}[P] - \underline{p}
&= \int_{\underline{p}}^{\bar{p}} (1 - F_\pi(p)) \, dp \end{equation}
\end{align} where $\mathbb{E}[P]$ is the expected price charged by the policy and $\underline{p}$ is the minimum viable price (marginal cost).
where $F_\pi(p)$ is the cumulative distribution function of prices generated by $\pi$ under standard operating conditions. % 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} \end{definition}
\begin{figure}[ht] \begin{figure}[ht]
@@ -81,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. 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] \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. 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} \end{theorem}
\begin{proof} \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)$. 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 survival function (or reliability function) of the minimum price is given by: The probability that the minimum price exceeds some threshold $t$ is:
\begin{equation} \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} \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} \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} \end{equation}
Applying this to our pricing statistic where the lower bound is $\underline{p}$: Since the integrand vanishes as $N \to \infty$ for all $t > \underline{p}$, the integral converges to zero. Therefore:
\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}]$):
\begin{equation} \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} \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} \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. % 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.
@@ -131,14 +136,18 @@ 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. 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. 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} \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} \subsubsection{Online Dynamic Pricing}
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$): 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} \begin{equation}
@@ -152,21 +161,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. 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. % For our offline experimental setting, we generalize a master value function that can encompass different demand estimation and pricing strategies.
%
\begin{align} % \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)]} % 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} % \end{align}
%
We follow differnet substitutouns which will server as hyperparameters later on. % We evaluate different substitutions of this objective, which later serve as hyperparameters in the simulator.
\subsection{Experimental Design} \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.
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. Since users act with motivations, we define a pool of tasks (jobs to be done) and assign tasks randomly to participants.
% TODO: describe the task pool in detail here -- list the specific tasks used in the experiments
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.
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 $y \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] \begin{figure}[ht]
\resizebox{\columnwidth}{!}{% \resizebox{\columnwidth}{!}{%
@@ -175,7 +194,59 @@ Our approach can be well summarized by a three-stage division, first we intend t
\caption{Overview of the Dynamic Pricing Tasks.} \caption{Overview of the Dynamic Pricing Tasks.}
\end{figure} \end{figure}
Our web platform (developed in similar patterns as the RecSim by \textcite{ie_recsim_2019}) allows us to setup a controled environment in which we assign tasks to human and agentic actors which are then carried out. Each actor gets a browser assigned experiment identification which is persistent across possibly multiple session identifiers. We then group by experiments and extract all the session interactions (trajectories) which follow the schema formalized below. 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, 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} \subsubsection{Interaction Schema}
@@ -211,6 +282,8 @@ $\mathcal{A}_{\text{filter}}$ & \texttt{search}, \texttt{filter\_date}, \texttt{
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. 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.
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. 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. 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.
@@ -218,12 +291,14 @@ In addition to behavioral events, the platform logs price observations to a sepa
\subsection{Generative Contamination and Separability} \subsection{Generative Contamination and Separability}
To develop a robust pricing learner, 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 $y_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} To answer this, we compute average KL divergence between transition probability matrices. This statistic gives global separability and event-level diagnostics at the same time. In our balanced dataset (50\% human, 50\% agent), the average divergence is approximately $1.8$. To contextualize this divergence metric we compare with an intra-class comparison baseline of randomly selected transitions.
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 contextualize this figure a useful intra-class baseline is to randomly split D_H into two equal halves, estimate a kernel from each half, compute the same average KL statistic, and repeat for B bootstrap samples (e.g. B=100). The resulting null distribution (mean +/- std) gives the divergence expected purely from estimation noise at this sample size. A between-class KL substantially above this null confirms the separation is real and not a finite-sample artefact. In practice: for each of B splits, partition D_H 50/50 without replacement, run build_kernel() on each half, average the per-state KL values, and collect the B scores into a reference distribution to compare against the 1.8 figure.
\begin{definition}[Kullback-Leibler Divergence for Transition Distributions] \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: 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:
@@ -233,25 +308,28 @@ Let $P_e$ and $Q_e$ be categorical distributions over destination states followi
where $\mathcal{S}_e$ denotes the set of destination events that follow $e$ in the human trajectories. where $\mathcal{S}_e$ denotes the set of destination events that follow $e$ in the human trajectories.
\end{definition} \end{definition}
To obtain this statistic we aggregate state transitions by their triggering event $e$ and treat the normalized outgoing probabilities as the categorical distributions $P_e$ (human) and $Q_e$ (agent). The computation intersects the event labels observed in both datasets, then iterates over each label and accumulates the log-ratio score. In practice this is implemented exactly as in models: for each destination $k$ we multiply the human probability by the log of the probability ratio and add the result to the running sum. Large contributions (including the case where $Q_e(k)$ is near zero) point to intents, such as rapid checkout or repeated navigation, that the agent policy fails to reproduce and therefore drive the contamination analysis. 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 this divergence we train a contrastive learning method to estimate a weak probability of a given trajectory being an agent $f(\cdot) \to [0,1]$ which we can use as a leverage for a weighted sum. This is a first attempt at a more informed separability. 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} \subsubsection{Transition Probability Estimation}
\label{sec:tpe} \label{sec:tpe}
For both subsets, we model the session dynamics as a Markov Decision Process (MDP) and estimate the transition kernel $\mathcal{T}$. for each respective actor type we define $\hat{\mathcal{T}}_A$ and $\hat{\mathcal{T}}_H$ which are the general transition kernels subject to clustering into $\hat{\mathcal{T}}_y^i$ where $\forall i \in \text{behavioral clusters of } \hat{\mathcal{T}}_y$. This is done to avoid a lumping of all actor behavior and allows for more intral-class penalization. The probability of transitioning to state $s'$ given state $s$ is estimated via maximum likelihood: 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} \begin{equation}
\hat{P}(s' \mid s) = \frac{N(s, s')}{\sum_{k \in \mathcal{S}} N(s, k)} \hat{P}(s' \mid s) = \frac{N(s, s')}{\sum_{k \in \mathcal{S}} N(s, k)}
\end{equation} \end{equation}
where $N(s, s')$ is the count of observed transitions. This allows us to construct a \textit{Contamination Generator} $\mathcal{G}(\alpha)$. In addition, 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$. From these transition probabilities we can observe an important feature which contributes to a differentiating assumption, which is that the mouse-behavior of an agent is almost non existent and therefore not utilized as a distinguishing factor both in the prior separability nor in any feature engineering. 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] \begin{figure}[ht]
\centering \centering
\includegraphics[width=0.8\textwidth]{chapters/mdp_human.pdf} \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} \label{fig:human_mdp_viz}
\end{figure} \end{figure}
@@ -263,15 +341,14 @@ where $N(s, s')$ is the count of observed transitions. This allows us to constru
\end{figure} \end{figure}
\subsection{Stronger Classification} \subsection{Second-Stage Classification}
We re-map the current event schema semantically to the event schema of another dataset. Our contaminated dataset is then used in another classifier where we can now also apply better feature engineering on other features while assigning correct lables to the entire dataset so the new dataset can be contaminated with $\mathcal{G}$ under some different contamination ratio $\alpha$. After contamination, we run a second classification stage. We remap events into a semantically aligned feature space, apply richer feature engineering, and retrain to obtain cleaner label probabilities across the full dataset. This classifier is then used directly in the reinforcement-learning reward structure.
This new classified can then be used in the reinforcement learning reward structure.
\subsection{Distributionally Robust Reinforcement Learning (DR-RL)} \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. To formulate the entire dependency chain from the trajctory $\tau^\prime$ which is a newly observed trajectory observed by the platform and generated by an unknown actor type (sampled over a behavioral profile defined in section \ref{sec:tpe}). As part of the dynamic pricing we need a mapping of demand parameterized by a trajectory and a price $\hat{Q}(p, \tau^\prime)$. For an observed trajectory we compute a new $\hat{\mathcal{T}}^\prime$ and using a baseline controlled observations of both $\bar{\mathcal{T}}_H$ and $\bar{\mathcal{T}}_A$ we can compute during inference time the following: 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} \begin{align}
\label{eq:delta_H} \label{eq:delta_H}
@@ -280,30 +357,57 @@ We formulate the pricing problem as a Stackelberg Game where the Platform (Leade
\Delta_A &= D_{KL}(\hat{\mathcal{T}}^\prime \parallel \bar{\mathcal{T}}_A) \Delta_A &= D_{KL}(\hat{\mathcal{T}}^\prime \parallel \bar{\mathcal{T}}_A)
\end{align} \end{align}
This creates two centroid-like heuristics which can on a per-session granularity basis guide our mixing paramtere $\alpha$. 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} \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} \begin{equation}
\mathcal{U}_\epsilon(\hat{P}_N) = \left\{ Q \in \mathcal{P}(\Xi) : W_p(Q, \hat{P}_N) \le \epsilon \right\} \mathcal{U}_\epsilon(\hat{P}_N) = \left\{ Q \in \mathcal{P}(\Xi) : W_p(Q, \hat{P}_N) \le \epsilon \right\}
\end{equation} \end{equation}
This set captures all distributions that are statistically close to our observed training data but allows for adversarial shifts. 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{The Min-Max Objective} \subsubsection{The Min-Max Objective}
The robust policy $\pi^*$ is obtained by solving the maximin problem: The robust policy $\pi^*$ is obtained by solving the maximin problem:
\begin{equation} \begin{equation}
\label{eq:robust_policy} \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}(p) \right] \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} \end{equation}
where $R(p, d)$ is the revenue function and $\lambda$ weighs the penalty for information leakage (COI). We previously defined $\text{COI}$, however to properly connect this concept into the reward structure we need to define a parametrized version which informs us of the leakage of said structure with $\text{COI}(p)$. where $R(p, d)$ is the revenue function and $\lambda$ weighs the information-leakage penalty.
Another proposed formulation of the optimal policy would be to adjust the ambiguity set dyanmically over the live computed divergence where $\epsilon(\Delta_H)$ to adjust the ball around or estimator according to each behavioral signal emited through a given trajctory. We state this as a possibility but do not peruse it due to literature suggesting that wesserstine methods do not require absolute continuity and are better with ``black swans'' \parencite{kuhn_wasserstein_2024}. 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} \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] \begin{figure}[ht]
\centering \centering
@@ -313,53 +417,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.} \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} \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.
\subsubsection{Pricing Mechanism Summary} \subsubsection{Pricing Mechanism Summary}
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_pricing_loop} 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 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.
\begin{algorithm}[t] \begin{algorithm}[t]
\caption{PHANTOM defensive pricing loop (bachelor-thesis level)} \caption{PHANTOM defensive pricing loop}
\label{alg:phantom_loop_clean} \label{alg:phantom_loop_clean}
\DontPrintSemicolon \DontPrintSemicolon
\SetKwInOut{Input}{Input}\SetKwInOut{Output}{Output} \SetKwInput{Input}{Input}
\SetKwInput{Output}{Output}
\Input{catalog size \(N\); costs \(c\); reference prices \(p^{ref}\); behavior models \(\bar T_H,\bar T_A\);
action weights \(\omega\); penalty \(\lambda\); horizon \(T\); sessions per step \(M\)}
\Output{price/demand trajectory \(\{(p_t,\hat Q_t,\hat\alpha_t)\}_{t=0}^{T-1}\)}
Initialize contamination estimate \(\hat\alpha \leftarrow 0.2\)\;
\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\)}{ \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])\)\;
set \(p_t \leftarrow \pi(\cdot) \) %c + (1 - \kappa \hat\alpha)\,(p^{ref}-c)\)\; define local ambiguity interval \(\mathcal{A}_{\epsilon_\alpha}(\alpha_0)=\{\alpha:\lvert\alpha-\alpha_0\rvert\le\epsilon_\alpha\}\)\;
and clip \(p_t\) to a feasible range (e.g., near cost up to a max margin)\; \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\)\;
\(\hat Q_t \leftarrow 0\), \(\mathcal S_t \leftarrow \emptyset\); \tcp{Observe sessions and compute demand proxy (Eq.~2)} compute demand proxy \(\hat Q_t^{(k)} = \sum_{m=1}^{M}\sum_j \omega(a_{m,j})\,\mathbf{1}[i_{m,j}=i]\)\;
\For{\(m \leftarrow 1\) \KwTo \(M\)}{ compute \((\Delta_H^{(k)},\Delta_A^{(k)})\) and session score \(f_t^{(k)}\) from KL divergence\;
sample a session trajectory \(\tau_m\) using \(\bar T_H\) or \(\bar T_A\)\; compute candidate reward \(r_t^{(k)} = R(p_t,\hat Q_t^{(k)}) - \lambda\,f_t^{(k)}\,c_{info}\)\;
\(\hat Q_t \leftarrow \hat Q_t + \sum_{k}\omega(a_{m,k})\)\;
\(\mathcal S_t \leftarrow \mathcal S_t \cup \{\tau_m\}\)\;
} }
choose \(k^* \leftarrow \arg\min_k r_t^{(k)}\), set \(\alpha_t^* \leftarrow \alpha_{k^*}\)\;
\tcp{Estimate contamination from behavioral separability} set \(\hat Q_t \leftarrow \hat Q_t^{(k^*)}\), \(r_t \leftarrow r_t^{(k^*)}\)\;
compute \(\hat\alpha \leftarrow \frac{1}{M}\sum_{\tau\in\mathcal S_t} \Big[\sigma\big(\beta(\Delta_H(\tau)-\Delta_A(\tau))\big)\Big]\)\;
compute \(J_t \leftarrow \text{Revenue}(p_t,\hat Q_t) - \lambda\cdot \text{COILeak}(\hat\alpha)\)\;
} }
\end{algorithm} \end{algorithm}
The algorithm operates in discrete epochs indexed by $t$. At each epoch, the platform publishes prices (leader move), observes the resulting session trajectories (follower response), and updates its contamination estimate based on behavioral divergence from the learned human and agent transition kernels $\bar{\mathcal{T}}_H$ and $\bar{\mathcal{T}}_A$. The history buffer $\mathcal{L}$ (termed ``Limbo'' in our implementation) enforces the alternating Stackelberg structure by maintaining the temporal sequence of price publications and demand observations. 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 a contamination-aware margin shrinkage: as the estimated agent contamination $\hat{\alpha}_t$ increases, the margin $(p^{\mathrm{ref}} - c)$ is proportionally reduced by factor $\kappa \in [0,1]$, with projection $\Pi_{\mathcal{P}}$ ensuring prices remain within the feasible set $\mathcal{P}$. In subsequent experiments, this heuristic update is replaced by the DR-RL policy $\pi^*$ from Eq.~\ref{eq:robust_policy}, which optimizes against the Wasserstein ambiguity set $\mathcal{U}_\epsilon$ rather than relying on a fixed margin adjustment rule. %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}.
\section{Heuristics as part of neuro-inspired steering systems}
Steve Burns, superior culliculus (face heuristics) we create this sort of part of the 'brain' + amortized inference.
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.
\section{Market construction}

View File

@@ -1,4 +1,10 @@
\section{Results} \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} \subsection{Behavioral Analysis}

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}

File diff suppressed because it is too large Load Diff

View File

@@ -49,11 +49,11 @@
\node[greenbox, minimum width=3.5cm] (commerce) at (-3.5, 2) {Commerce Experiment}; \node[greenbox, minimum width=3.5cm] (commerce) at (-3.5, 2) {Commerce Experiment};
\node[greenbox, minimum width=1.5cm] (raw) at (-6.5, 0) {Raw\\Logs}; \node[greenbox, minimum width=1.5cm] (raw) at (-6.5, 0) {Raw\\Logs};
\node[greenbox, minimum width=1.5cm] (features) at (-4, -2.5) {Features}; \node[greenbox, minimum width=1.5cm] (features) at (-4, -2.5) {Features};
\node[greenbox, minimum width=2.5cm] (classification) at (-1, -0.5) {Classification\\Training A/H}; \node[greenbox, minimum width=2.5cm] (classification) at (-0.8, 0) {Classification\\Training A/H};
% Right Loop (Blue) Nodes % Right Loop (Blue) Nodes
\node[bluebox, minimum width=2.5cm] (trainedpricing) at (3.2, 2) {Trained Pricing}; \node[bluebox, minimum width=2.5cm] (trainedpricing) at (3.2, 2) {Trained Pricing};
\node[bluebox, minimum width=2.5cm] (policy) at (6.5, 0) {Trained Pricing\\Policy}; \node[bluebox, minimum width=1.5cm] (policy) at (6.5, 0) {Trained\\Pricing\\Policy};
\node[bluebox, minimum width=2.5cm] (rlgym) at (3.2, -2.2) {RL Gym\\Training}; \node[bluebox, minimum width=2.5cm] (rlgym) at (3.2, -2.2) {RL Gym\\Training};
% --- Background Dashed Loops --- % --- Background Dashed Loops ---

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,24 @@
from PIL import Image, ImageDraw, ImageFont
text = open("banner.txt", "r", encoding="utf-8").read()
scale = 4 # 26 is typical
pad = 10
font_px = 18
font = ImageFont.truetype("DejaVuSansMono.ttf", font_px * scale)
# Measure at high res
dummy = Image.new("RGB", (1, 1), "white")
d = ImageDraw.Draw(dummy)
bbox = d.multiline_textbbox((0, 0), text, font=font)
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
# Render at high res
hi = Image.new("RGB", (w + 2*pad*scale, h + 2*pad*scale), "white")
d = ImageDraw.Draw(hi)
d.multiline_text((pad*scale, pad*scale), text, font=font, fill="black")
# Downscale with a good filter
out = hi.resize((hi.width // scale, hi.height // scale), resample=Image.Resampling.LANCZOS)
out.save("banner.png", dpi=(300, 300))

View File

@@ -0,0 +1,23 @@
Actors Trajectories
■════■ interact ┌────────────┐ ┌──┐
║Agent──────┬──────▻Web Platform├──┐ │τ1│ ┌▻Q (demand estimate)─┐
║Human──────┘ └──────△─────┘ └──▻..│──┘ │
╚════■ │ │τK│ │
△ │ └──┘ │
│motivate │ │
└────────┐ │Setting ┌──┐ Pricing Engine │
▲ ┌──┐│ │Prices │p1│ ┌──────────────┐ │
│ ┌─┘ ││ └───────────┤..│◅────│▒▒▒▒▒▒▒▒▒▒▒▒▒▒│◅──┘
│ │ └──┐ │pN│ └─────┬──┬─────┘
│ ┌─┘ │ └──┘ │ │
└─┴─────────┴─▶ │ │
Private Valuations │ │
│ │
╔═══════════════════════════════════════════════════╧══╧════════╗
║ Training Loop / SAC PPO DQN A2C ║
║ ■═════════════════════════════■ ║
║ Q̂_t,i = Σ_s Σ_k ω(a_s,k) · 1[i_s,k = i] │ ║
║ f(τ') from KL( T' || T_H ) and KL( T' || T_A ) │ ║
α* = argmin_{α ∈ Aε(α0)} [ Revenue(p, Q^α) - λ·COI_leak ] │ ║
║ r_t = Revenue - λ·f(τ') | a* ▽ ║
╚═══════════════════════════════════════════════════════════════╝

View File

@@ -7,7 +7,7 @@
\begin{titlepage} \begin{titlepage}
\centering \centering
\includegraphics[width=0.3\textwidth]{graphics/SST.png}\\[1cm] \includegraphics[width=\textwidth]{graphics/banner.png}\\[0.8cm]
\LARGE\textbf{PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms}\\[0.5cm] \LARGE\textbf{PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms}\\[0.5cm]
\Large\textbf{Daniel Rösel}\\ \Large\textbf{Daniel Rösel}\\
\large\textit{Bachelor of Computer Science \& Artificial Intelligence}\\[0.5cm] \large\textit{Bachelor of Computer Science \& Artificial Intelligence}\\[0.5cm]
@@ -27,7 +27,7 @@ These behavioral signals serve as inputs for a Distributionally Robust Reinforce
\noindent\textbf{Keywords:} Dynamic Pricing, LLM Agents, Adversarial Machine Learning, E-commerce, Behavioral Detection, Reinforcement Learning \noindent\textbf{Keywords:} Dynamic Pricing, LLM Agents, Adversarial Machine Learning, E-commerce, Behavioral Detection, Reinforcement Learning
\vspace{1em} \vspace{1em}
\noindent\textbf{Acknowledgments:} Eugene Bykovets, PhD - ETH for helping with problem formulation. This research was supported by the TPU Research Cloud program. \noindent\textbf{Acknowledgments:} This research was supported by the TPU Research Cloud program, which provided access to Google Cloud TPU accelerators (including TPU v4, v5e, and v6e).
\clearpage \clearpage
\input{chapters/01-intro} \input{chapters/01-intro}

View File

@@ -29,6 +29,8 @@
\usepackage{subcaption} \usepackage{subcaption}
\usepackage{siunitx} \usepackage{siunitx}
\usepackage{tikz} \usepackage{tikz}
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}
\usepackage{listings} \usepackage{listings}
\usepackage{xcolor} \usepackage{xcolor}
\usepackage[ruled,vlined]{algorithm2e} \usepackage[ruled,vlined]{algorithm2e}

View File

@@ -12,3 +12,4 @@ uv
scikit-learn scikit-learn
supabase supabase
pymc pymc
wandb

View File

@@ -1,5 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@layer base { @layer base {
:root { :root {
--background: #ffffff; --background: #ffffff;
@@ -16,6 +18,7 @@
--spacing-lg: 32px; --spacing-lg: 32px;
--border-radius: 8px; --border-radius: 8px;
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.1); --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.1);
color-scheme: light;
} }
} }
@@ -27,13 +30,6 @@
} }
@layer base { @layer base {
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@@ -43,6 +39,7 @@
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
color-scheme: light;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6; line-height: 1.6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;