mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
initial progress
This commit is contained in:
@@ -23,7 +23,7 @@ where:
|
|||||||
The platform does not directly observe the true underlying demand function $d(p)$. Instead, it observes a behavioral proxy $\hat{q}_t$, which is a composite signal derived from the mixture of actor types. We define the demand proxy for product $i$ at epoch $t$ as a weighted aggregation of events:
|
The platform does not directly observe the true underlying demand function $d(p)$. Instead, it observes a behavioral proxy $\hat{q}_t$, which is a composite signal derived from the mixture of actor types. We define the demand proxy for product $i$ at epoch $t$ as a weighted aggregation of events:
|
||||||
\begin{equation}
|
\begin{equation}
|
||||||
\label{eq:qhat}
|
\label{eq:qhat}
|
||||||
\hat{q}_{t,i} = \sum_{s \in \mathcal{S}_t} \sum_{k=1}^{L_s} \omega(a_{s,k}) \cdot \mathds{1}[i_{s,k} = i]
|
\hat{q}_{t,i} = \sum_{s \in \mathcal{S}_t} \sum_{k=1}^{L_s} \omega(a_{s,k}) \cdot \mathbf{1}[i_{s,k} = i]
|
||||||
\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.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,15 +4,34 @@ set -euo pipefail
|
|||||||
|
|
||||||
cmd="${1:-}"
|
cmd="${1:-}"
|
||||||
|
|
||||||
|
sync_mdp_figures() {
|
||||||
|
local script_dir project_root sim_dir chapters_dir
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
project_root="$(cd "$script_dir/.." && pwd)"
|
||||||
|
sim_dir="$project_root/sim/rl/behavior_loader"
|
||||||
|
chapters_dir="$project_root/paper/src/chapters"
|
||||||
|
|
||||||
|
printf '%s\n' 'Refreshing MDP figures for paper...'
|
||||||
|
(
|
||||||
|
cd "$sim_dir"
|
||||||
|
python models.py
|
||||||
|
)
|
||||||
|
|
||||||
|
cp "$sim_dir/human_mdp_viz.pdf" "$chapters_dir/mdp_human.pdf"
|
||||||
|
cp "$sim_dir/agent_mdp_viz.pdf" "$chapters_dir/mdp_agent.pdf"
|
||||||
|
}
|
||||||
|
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
build)
|
build)
|
||||||
mkdir -p paper/build
|
mkdir -p paper/build
|
||||||
|
sync_mdp_figures
|
||||||
bash paper/concat_code.sh
|
bash paper/concat_code.sh
|
||||||
cd paper/src
|
cd paper/src
|
||||||
latexmk -pdf -jobname=main -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main.tex
|
latexmk -pdf -jobname=main -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main.tex
|
||||||
;;
|
;;
|
||||||
watch)
|
watch)
|
||||||
mkdir -p paper/build
|
mkdir -p paper/build
|
||||||
|
sync_mdp_figures
|
||||||
cd paper/src
|
cd paper/src
|
||||||
latexmk -pvc -pdf -jobname=main -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main.tex
|
latexmk -pvc -pdf -jobname=main -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main.tex
|
||||||
;;
|
;;
|
||||||
@@ -33,11 +52,13 @@ case "$cmd" in
|
|||||||
;;
|
;;
|
||||||
build-genpop)
|
build-genpop)
|
||||||
mkdir -p paper/build
|
mkdir -p paper/build
|
||||||
|
sync_mdp_figures
|
||||||
cd paper/src
|
cd paper/src
|
||||||
latexmk -pdf -jobname=main-genpop -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main-genpop.tex
|
latexmk -pdf -jobname=main-genpop -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main-genpop.tex
|
||||||
;;
|
;;
|
||||||
watch-genpop)
|
watch-genpop)
|
||||||
mkdir -p paper/build
|
mkdir -p paper/build
|
||||||
|
sync_mdp_figures
|
||||||
cd paper/src
|
cd paper/src
|
||||||
latexmk -pvc -pdf -jobname=main-genpop -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main-genpop.tex
|
latexmk -pvc -pdf -jobname=main-genpop -f -interaction=nonstopmode -file-line-error -r ../.latexmkrc -outdir=../build main-genpop.tex
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -209,19 +209,94 @@ def _resolve_event_order(
|
|||||||
return sorted(observed)
|
return sorted(observed)
|
||||||
|
|
||||||
|
|
||||||
def _fixed_circle_positions(
|
def _compass_from_angle(angle_rad: float) -> str:
|
||||||
events: List[str], radius: float
|
ports = ("e", "ne", "n", "nw", "w", "sw", "s", "se")
|
||||||
|
normalized = (angle_rad + (2 * np.pi)) % (2 * np.pi)
|
||||||
|
step = np.pi / 4
|
||||||
|
idx = int(np.round(normalized / step)) % len(ports)
|
||||||
|
return ports[idx]
|
||||||
|
|
||||||
|
|
||||||
|
def _edge_ports(
|
||||||
|
src: str,
|
||||||
|
dst: str,
|
||||||
|
positions: Dict[str, Tuple[float, float]],
|
||||||
|
has_reverse: bool,
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
src_x, src_y = positions[src]
|
||||||
|
dst_x, dst_y = positions[dst]
|
||||||
|
angle = float(np.arctan2(dst_y - src_y, dst_x - src_x))
|
||||||
|
|
||||||
|
if has_reverse:
|
||||||
|
bend = np.pi / 10
|
||||||
|
angle += bend if src < dst else -bend
|
||||||
|
|
||||||
|
tail_port = _compass_from_angle(angle)
|
||||||
|
head_port = _compass_from_angle(angle + np.pi)
|
||||||
|
return tail_port, head_port
|
||||||
|
|
||||||
|
|
||||||
|
def _edge_style(prob: float) -> Dict[str, str]:
|
||||||
|
if prob >= 0.75:
|
||||||
|
edge_color = "#111827"
|
||||||
|
elif prob >= 0.50:
|
||||||
|
edge_color = "#374151"
|
||||||
|
elif prob >= 0.25:
|
||||||
|
edge_color = "#6b7280"
|
||||||
|
else:
|
||||||
|
edge_color = "#9ca3af"
|
||||||
|
return {
|
||||||
|
"color": edge_color,
|
||||||
|
"fontcolor": "#111827",
|
||||||
|
"fontsize": "10",
|
||||||
|
"penwidth": f"{0.9 + 3.6 * prob:.2f}",
|
||||||
|
"arrowsize": f"{0.55 + 0.55 * prob:.2f}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _format_node_label(evt: str) -> str:
|
||||||
|
max_line_len = 16
|
||||||
|
tokens = evt.split("_")
|
||||||
|
if len(tokens) == 1:
|
||||||
|
return evt
|
||||||
|
|
||||||
|
lines: List[str] = []
|
||||||
|
curr = ""
|
||||||
|
for token in tokens:
|
||||||
|
piece = token if not curr else f"_{token}"
|
||||||
|
if curr and len(curr) + len(piece) > max_line_len:
|
||||||
|
lines.append(curr)
|
||||||
|
curr = token
|
||||||
|
else:
|
||||||
|
curr = f"{curr}{piece}" if curr else token
|
||||||
|
if curr:
|
||||||
|
lines.append(curr)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_flow_positions(
|
||||||
|
events: List[str],
|
||||||
|
layout_radius: float,
|
||||||
) -> Dict[str, Tuple[float, float]]:
|
) -> Dict[str, Tuple[float, float]]:
|
||||||
|
"""Balanced grid layout for paper-friendly diagrams."""
|
||||||
if not events:
|
if not events:
|
||||||
return {}
|
return {}
|
||||||
step = (2 * np.pi) / len(events)
|
|
||||||
return {
|
num_events = len(events)
|
||||||
evt: (
|
cols = int(np.ceil(np.sqrt(num_events)))
|
||||||
float(radius * np.cos(idx * step)),
|
rows = int(np.ceil(num_events / cols))
|
||||||
float(radius * np.sin(idx * step)),
|
x_step = max(layout_radius * 1.10, 3.6)
|
||||||
)
|
y_step = max(layout_radius * 0.95, 3.2)
|
||||||
for idx, evt in enumerate(events)
|
|
||||||
}
|
positions: Dict[str, Tuple[float, float]] = {}
|
||||||
|
for idx, evt in enumerate(events):
|
||||||
|
row = idx // cols
|
||||||
|
col = idx % cols
|
||||||
|
x = (col - (cols - 1) / 2.0) * x_step
|
||||||
|
y = ((rows - 1) / 2.0 - row) * y_step
|
||||||
|
positions[evt] = (float(x), float(y))
|
||||||
|
|
||||||
|
return positions
|
||||||
|
|
||||||
|
|
||||||
def visualize_mdp(
|
def visualize_mdp(
|
||||||
@@ -232,35 +307,79 @@ def visualize_mdp(
|
|||||||
view: bool = False,
|
view: bool = False,
|
||||||
export_dot: bool = False,
|
export_dot: bool = False,
|
||||||
event_order: Optional[List[str]] = None,
|
event_order: Optional[List[str]] = None,
|
||||||
layout_radius: float = 6.0,
|
layout_radius: float = 10.0,
|
||||||
node_diameter: float = 2.4,
|
node_diameter: float = 1.8,
|
||||||
|
label_threshold: float = 0.08,
|
||||||
):
|
):
|
||||||
if not model.mdp:
|
if not model.mdp:
|
||||||
raise ValueError("build MDP first")
|
raise ValueError("build MDP first")
|
||||||
|
|
||||||
evt_trans = aggregate_event_transitions(model.mdp)
|
evt_trans = aggregate_event_transitions(model.mdp)
|
||||||
ordered_events = _resolve_event_order(evt_trans, event_order=event_order)
|
ordered_events = _resolve_event_order(evt_trans, event_order=event_order)
|
||||||
positions = _fixed_circle_positions(ordered_events, radius=layout_radius)
|
positions = _compute_flow_positions(ordered_events, layout_radius=layout_radius)
|
||||||
|
|
||||||
g = graphviz.Digraph(format=fmt, engine="neato")
|
g = graphviz.Digraph(format=fmt, engine="neato")
|
||||||
g.attr(overlap="false", splines="true", outputorder="edgesfirst")
|
g.attr(
|
||||||
|
overlap="false",
|
||||||
|
splines="true",
|
||||||
|
outputorder="edgesfirst",
|
||||||
|
pad="0.5",
|
||||||
|
sep="+9",
|
||||||
|
esep="+4",
|
||||||
|
bgcolor="white",
|
||||||
|
dpi="180",
|
||||||
|
)
|
||||||
g.attr(
|
g.attr(
|
||||||
"node",
|
"node",
|
||||||
shape="circle",
|
shape="circle",
|
||||||
|
fixedsize="true",
|
||||||
width=f"{node_diameter:.2f}",
|
width=f"{node_diameter:.2f}",
|
||||||
height=f"{node_diameter:.2f}",
|
height=f"{node_diameter:.2f}",
|
||||||
fixedsize="true",
|
fontsize="11",
|
||||||
fontsize="10",
|
fontname="Helvetica",
|
||||||
|
style="filled",
|
||||||
|
fillcolor="white",
|
||||||
|
color="#374151",
|
||||||
|
fontcolor="#111827",
|
||||||
|
penwidth="1.8",
|
||||||
|
peripheries="1",
|
||||||
|
)
|
||||||
|
g.attr(
|
||||||
|
"edge",
|
||||||
|
fontname="Helvetica",
|
||||||
)
|
)
|
||||||
|
|
||||||
for evt in ordered_events:
|
for evt in ordered_events:
|
||||||
x_pos, y_pos = positions[evt]
|
x, y = positions[evt]
|
||||||
g.node(evt, pos=f"{x_pos:.3f},{y_pos:.3f}!", pin="true")
|
g.node(evt, label=_format_node_label(evt), pos=f"{x:.2f},{y:.2f}!", pin="true")
|
||||||
|
|
||||||
for src, dsts in evt_trans.items():
|
edges = [
|
||||||
for dst, prob in dsts.items():
|
(src, dst, prob)
|
||||||
if prob > threshold:
|
for src, dsts in evt_trans.items()
|
||||||
g.edge(src, dst, label=f"{prob:.2f}")
|
for dst, prob in dsts.items()
|
||||||
|
if prob > threshold
|
||||||
|
]
|
||||||
|
edge_set = {(src, dst) for src, dst, _ in edges}
|
||||||
|
|
||||||
|
for src, dst, prob in sorted(edges, key=lambda row: row[2]):
|
||||||
|
edge_attrs: Dict[str, str] = _edge_style(prob)
|
||||||
|
|
||||||
|
if src == dst:
|
||||||
|
# pick a loop port away from the main flow
|
||||||
|
sx, sy = positions[src]
|
||||||
|
loop_port = "n" if sy <= 0 else "s"
|
||||||
|
edge_attrs.update({"tailport": loop_port, "headport": loop_port})
|
||||||
|
else:
|
||||||
|
has_reverse = (dst, src) in edge_set
|
||||||
|
tail_port, head_port = _edge_ports(src, dst, positions, has_reverse)
|
||||||
|
edge_attrs.update({"tailport": tail_port, "headport": head_port})
|
||||||
|
if has_reverse:
|
||||||
|
edge_attrs["constraint"] = "false"
|
||||||
|
|
||||||
|
if prob >= label_threshold or src == dst:
|
||||||
|
edge_attrs["label"] = f" {prob:.2f} "
|
||||||
|
|
||||||
|
g.edge(src, dst, **edge_attrs)
|
||||||
|
|
||||||
g.render(output, view=view, cleanup=True)
|
g.render(output, view=view, cleanup=True)
|
||||||
print(f"Saved MDP graph to {output}.{fmt}")
|
print(f"Saved MDP graph to {output}.{fmt}")
|
||||||
|
|||||||
Reference in New Issue
Block a user