Skip to content

Commit 1719bc5

Browse files
committed
Minor viz added
1 parent 5bdee70 commit 1719bc5

2 files changed

Lines changed: 133 additions & 3 deletions

File tree

codeclash/analysis/viz/cdf_thought_length_per_round.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,16 @@ def main():
5454
model_to_steps = json.load(f)
5555

5656
# Plot CDF
57-
plt.figure(figsize=(10, 6))
57+
plt.figure(figsize=(8, 8))
5858
for model, thought_length in model_to_steps.items():
5959
sorted_steps = sorted(thought_length)
6060
yvals = [i / len(sorted_steps) for i in range(len(sorted_steps))]
6161
plt.step(sorted_steps, yvals, label=MODEL_TO_DISPLAY_NAME[model], where="post", color=MODEL_TO_COLOR[model])
6262

6363
LIM = 200
6464
plt.xlim(0, LIM)
65-
plt.xticks(range(0, LIM + 1, 20), fontsize=12, fontproperties=FONT_REG)
66-
plt.yticks([i / 10 for i in range(11)], [f"{i * 10}%" for i in range(11)], fontsize=12, fontproperties=FONT_REG)
65+
plt.xticks(range(0, LIM + 1, 20), fontsize=18, fontproperties=FONT_REG)
66+
plt.yticks([i / 10 for i in range(11)], [f"{i * 10}%" for i in range(11)], fontsize=18, fontproperties=FONT_REG)
6767
plt.xlabel("Thought length (in words) per action", fontproperties=FONT_BOLD, fontsize=18)
6868
# plt.ylabel("Cumulative Probability")
6969
# plt.title("CDF of Thought Length (in Words) per Round by Model")
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import re
4+
5+
from matplotlib import pyplot as plt
6+
from tqdm.auto import tqdm
7+
8+
from codeclash.analysis.viz.utils import ASSETS_DIR, FONT_BOLD, MODEL_TO_COLOR, MODEL_TO_DISPLAY_NAME
9+
from codeclash.constants import LOCAL_LOG_DIR
10+
11+
ROUNDS = 15
12+
OUTPUT_FILE = ASSETS_DIR / "line_chart_thought_length_per_round.png"
13+
DATA_CACHE = ASSETS_DIR / "line_chart_thought_length_per_round.json"
14+
15+
16+
def extract_thought_length_from_message(content: str) -> int | None:
17+
"""Return number of words in THOUGHT section of message content, or None if not found."""
18+
thought_match = re.search(r"THOUGHT:(.+?)```bash", content, re.DOTALL | re.IGNORECASE)
19+
if not thought_match:
20+
return None
21+
thought = thought_match.group(1).strip()
22+
return len(thought.split())
23+
24+
25+
def main():
26+
model_to_round_thoughts: dict[str, list[list[int]]] = {}
27+
28+
if not DATA_CACHE.exists():
29+
tournaments = [x.parent for x in LOCAL_LOG_DIR.rglob("metadata.json")]
30+
for game_log_folder in tqdm(tournaments, desc="Scanning tournaments"):
31+
try:
32+
with open(game_log_folder / "metadata.json") as f:
33+
metadata = json.load(f)
34+
except Exception:
35+
continue
36+
37+
try:
38+
p2m = {x["name"]: x["config"]["model"]["model_name"].strip("@") for x in metadata["config"]["players"]}
39+
except Exception:
40+
continue
41+
42+
# ensure models exist
43+
for model in set(p2m.values()):
44+
model_to_round_thoughts.setdefault(model, [[] for _ in range(ROUNDS)])
45+
46+
for player_name, model in p2m.items():
47+
traj_files = (game_log_folder / "players" / player_name).rglob("*.traj.json")
48+
for traj_file in traj_files:
49+
m = traj_file.name.rsplit("_r", 1)
50+
if len(m) != 2:
51+
continue
52+
try:
53+
round_part = m[1]
54+
round_idx = int(round_part.split(".")[0])
55+
except Exception:
56+
continue
57+
if round_idx < 1 or round_idx > ROUNDS:
58+
continue
59+
60+
try:
61+
with open(traj_file) as f:
62+
traj = json.load(f)
63+
except Exception:
64+
continue
65+
66+
for message in traj.get("messages", []):
67+
if message.get("role") != "assistant":
68+
continue
69+
content = message.get("content", "")
70+
thought_len = extract_thought_length_from_message(content)
71+
if thought_len is None:
72+
continue
73+
model_to_round_thoughts[model][round_idx - 1].append(thought_len)
74+
75+
# write cache
76+
with open(DATA_CACHE, "w") as f:
77+
json.dump(model_to_round_thoughts, f, indent=2)
78+
79+
# load cache
80+
with open(DATA_CACHE) as f:
81+
model_to_round_thoughts = json.load(f)
82+
83+
# Compute averages per round (trim top 1% outliers, keep at least one element)
84+
model_to_avg: dict[str, list[float]] = {}
85+
for model, rounds_lists in model_to_round_thoughts.items():
86+
# pad/truncate
87+
if len(rounds_lists) < ROUNDS:
88+
rounds_lists = rounds_lists + [[] for _ in range(ROUNDS - len(rounds_lists))]
89+
avgs: list[float] = []
90+
for lst in rounds_lists[:ROUNDS]:
91+
if lst:
92+
nums = [int(x) for x in lst]
93+
sorted_nums = sorted(nums)
94+
cutoff_index = max(1, int(len(sorted_nums) * 0.99))
95+
filtered = sorted_nums[:cutoff_index]
96+
avgs.append(sum(filtered) / len(filtered) if filtered else 0.0)
97+
else:
98+
avgs.append(0.0)
99+
model_to_avg[model] = avgs
100+
101+
# Print summary
102+
print("Average thought length (words) per round (first 15 rounds):")
103+
for model, avgs in model_to_avg.items():
104+
display = MODEL_TO_DISPLAY_NAME.get(model, model)
105+
print(f" - {display} ({model}): " + ", ".join([f"{v:.1f}" for v in avgs]))
106+
107+
# Plot
108+
plt.figure(figsize=(8, 8))
109+
x = list(range(1, ROUNDS + 1))
110+
ymax = 0
111+
for model, avgs in model_to_avg.items():
112+
display = MODEL_TO_DISPLAY_NAME.get(model, model)
113+
color = MODEL_TO_COLOR.get(model, None)
114+
plt.plot(x, avgs, marker="o", label=display, linewidth=1.5, markersize=6, color=color)
115+
ymax = max(ymax, max(avgs) if avgs else 0)
116+
117+
plt.xlabel("Round", fontsize=18, fontproperties=FONT_BOLD)
118+
plt.xticks(x, fontproperties=FONT_BOLD, fontsize=18)
119+
plt.yticks(fontproperties=FONT_BOLD, fontsize=18)
120+
# FONT_BOLD.set_size(12)
121+
# plt.legend(prop=FONT_BOLD, loc="upper right")
122+
plt.grid(True, alpha=0.3)
123+
plt.ylim(0, max(10, ymax + 5))
124+
plt.tight_layout()
125+
plt.savefig(OUTPUT_FILE, dpi=300, bbox_inches="tight")
126+
print(f"Saved line chart to {OUTPUT_FILE}")
127+
128+
129+
if __name__ == "__main__":
130+
main()

0 commit comments

Comments
 (0)