Skip to content

Commit 3221a5a

Browse files
committed
Add thought length per round, command diversity cdfs
1 parent fa2ba22 commit 3221a5a

5 files changed

Lines changed: 263 additions & 2 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""
2+
Command Diversity Analysis via CDF Visualization
3+
4+
This script analyzes the diversity of bash commands used by different AI models
5+
during CodeClash tournaments, visualized as a Cumulative Distribution Function (CDF).
6+
7+
WHAT THE METRIC MEASURES:
8+
- Command Diversity = Shannon entropy of command types used in each session
9+
- Higher entropy (2.5-3.5) = model uses many different command types
10+
- Lower entropy (1.5-2.5) = model tends to stick to fewer command types
11+
12+
HOW TO INTERPRET THE CDF:
13+
- X-axis: Shannon entropy values (diversity score)
14+
- Y-axis: Cumulative probability (fraction of sessions at or below that diversity)
15+
- Curves shifted RIGHT = more diverse command usage
16+
- Curves shifted LEFT = more focused/repetitive command usage
17+
- Steep curves = consistent behavior, gradual curves = variable behavior
18+
19+
STRATEGIC IMPLICATIONS:
20+
High diversity could indicate:
21+
- Better exploration of the codebase and problem space
22+
- More thorough debugging and analysis approaches
23+
- Adaptability to different types of challenges
24+
- OR potentially inefficient trial-and-error behavior
25+
26+
Low diversity could indicate:
27+
- Efficient, focused strategies with proven command patterns
28+
- Expertise with a smaller set of reliable tools
29+
- Consistent methodology across different problems
30+
- OR potentially limited problem-solving approaches or tool awareness
31+
32+
The "best" diversity level depends on the task complexity and whether exploration
33+
or exploitation is more valuable in the given context.
34+
"""
35+
36+
import json
37+
import math
38+
from collections import Counter
39+
40+
from matplotlib import pyplot as plt
41+
from tqdm.auto import tqdm
42+
43+
from codeclash.constants import LOCAL_LOG_DIR
44+
45+
OUTPUT_FILE = "cdf_command_diversity.png"
46+
47+
48+
def shannon_entropy(command_list):
49+
"""
50+
Calculate Shannon entropy for a list of commands.
51+
52+
Shannon entropy measures the "surprise" or information content in the distribution.
53+
- Higher entropy = more uniform distribution across command types (more diverse)
54+
- Lower entropy = some commands are much more frequent (more focused)
55+
- Max entropy = log2(unique_commands) when all commands equally frequent
56+
57+
Args:
58+
command_list: List of command strings (e.g., ['ls', 'cat', 'ls', 'grep'])
59+
60+
Returns:
61+
Float entropy value (0 = single command type, higher = more diverse)
62+
"""
63+
if not command_list:
64+
return 0.0
65+
66+
# Count frequency of each command type
67+
counter = Counter(command_list)
68+
total = len(command_list)
69+
entropy = 0.0
70+
71+
# Calculate Shannon entropy: -Σ(p * log2(p)) where p is probability of each command
72+
for count in counter.values():
73+
prob = count / total
74+
entropy -= prob * math.log2(prob)
75+
76+
return entropy
77+
78+
79+
def extract_commands_from_trajectory(traj):
80+
"""
81+
Extract all bash commands from a trajectory file.
82+
83+
Parses assistant messages to find bash code blocks and extracts the first word
84+
of each command as the "command type" (e.g., 'cat', 'ls', 'python', etc.).
85+
86+
Args:
87+
traj: Loaded trajectory JSON data
88+
89+
Returns:
90+
List of command type strings used in this session
91+
"""
92+
commands = []
93+
messages = traj.get("messages", [])
94+
95+
# Look through all assistant responses for bash commands
96+
for message in messages:
97+
if message.get("role") == "assistant":
98+
content = message.get("content", "")
99+
100+
# Extract bash command from ```bash code blocks using regex
101+
import re
102+
103+
bash_match = re.search(r"```(bash|sh)\n(.*?)\n```", content, re.DOTALL)
104+
if bash_match:
105+
command = bash_match.group(2).strip()
106+
# Get first word as command type (e.g., "cat file.txt" -> "cat")
107+
cmd_type = command.split()[0] if command else "unknown"
108+
commands.append(cmd_type)
109+
110+
return commands
111+
112+
113+
def main():
114+
"""
115+
Main analysis function that processes all tournament data and generates the CDF plot.
116+
117+
Process:
118+
1. Scan all tournament directories for trajectory files
119+
2. Extract commands from each trajectory and calculate diversity
120+
3. Group diversity scores by model
121+
4. Generate CDF visualization comparing models
122+
"""
123+
model_to_diversity = {}
124+
125+
# Find all tournament directories by looking for game.log files
126+
tournaments = [x.parent for x in LOCAL_LOG_DIR.rglob("game.log")]
127+
for game_log_folder in tqdm(tournaments):
128+
# Load tournament metadata to get player-to-model mapping
129+
with open(game_log_folder / "metadata.json") as f:
130+
metadata = json.load(f)
131+
try:
132+
# Extract mapping from player name to model name
133+
p2m = {x["name"]: x["config"]["model"]["model_name"].strip("@") for x in metadata["config"]["players"]}
134+
# Initialize diversity list for each model we encounter
135+
for model in p2m.values():
136+
if model not in model_to_diversity:
137+
model_to_diversity[model] = []
138+
except KeyError:
139+
# Skip tournaments with malformed metadata
140+
continue
141+
142+
# Process each player's trajectory files
143+
for name in p2m.keys():
144+
traj_files = (game_log_folder / "players" / name).rglob("*.traj.json")
145+
for traj_file in traj_files:
146+
try:
147+
with open(traj_file) as f:
148+
traj = json.load(f)
149+
150+
# Extract commands and calculate diversity for this session
151+
commands = extract_commands_from_trajectory(traj)
152+
if commands: # Only calculate entropy if there are commands
153+
diversity = shannon_entropy(commands)
154+
model_to_diversity[p2m[name]].append(diversity)
155+
except (json.JSONDecodeError, KeyError):
156+
# Skip malformed trajectory files
157+
continue
158+
159+
# Remove models with no valid data
160+
model_to_diversity = {k: v for k, v in model_to_diversity.items() if v}
161+
162+
# Print summary statistics for each model
163+
print("Command Diversity Summary:")
164+
for model, diversities in model_to_diversity.items():
165+
avg_diversity = sum(diversities) / len(diversities)
166+
max_diversity = max(diversities)
167+
print(
168+
f"- {model}: {len(diversities)} sessions; avg diversity {avg_diversity:.3f}; max diversity {max_diversity:.3f}"
169+
)
170+
171+
# Generate CDF plot comparing all models
172+
plt.figure(figsize=(12, 8))
173+
colors = plt.cm.tab10(range(len(model_to_diversity)))
174+
175+
for i, (model, diversities) in enumerate(model_to_diversity.items()):
176+
# Sort diversity values and create cumulative probability values
177+
sorted_diversities = sorted(diversities)
178+
yvals = [i / len(sorted_diversities) for i in range(len(sorted_diversities))]
179+
# Plot as step function (standard for CDFs)
180+
plt.step(sorted_diversities, yvals, label=model, where="post", color=colors[i])
181+
182+
plt.xlabel("Command Diversity (Shannon Entropy)")
183+
plt.ylabel("Cumulative Probability")
184+
plt.title("CDF of Command Diversity by Model\n(Higher entropy = more diverse command usage)")
185+
plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") # Legend outside plot area
186+
plt.grid(True, alpha=0.3)
187+
plt.tight_layout()
188+
plt.savefig(OUTPUT_FILE, dpi=300, bbox_inches="tight")
189+
print(f"Saved CDF plot to {OUTPUT_FILE}")
190+
191+
192+
if __name__ == "__main__":
193+
main()

codeclash/analysis/viz/cdf_files_edited_per_round.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010

1111
def main():
12-
# Create data structure of model -> list of files_edited per round
1312
model_to_num_files = {}
1413

1514
tournaments = [x.parent for x in LOCAL_LOG_DIR.rglob("game.log")]

codeclash/analysis/viz/cdf_steps_per_round.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010

1111
def main():
12-
# Create data structure of model -> list of steps per round
1312
model_to_steps = {}
1413

1514
tournaments = [x.parent for x in LOCAL_LOG_DIR.rglob("game.log")]
@@ -32,6 +31,12 @@ def main():
3231
num_steps = sum([1 for _ in traj["messages"] if _["role"] == "assistant"])
3332
model_to_steps[p2m[name]].append(num_steps)
3433

34+
for model, steps in model_to_steps.items():
35+
step_limit_exceeded = sum(1 for s in steps if s == 30) / len(steps) * 100
36+
print(
37+
f"- {model}: {len(steps)} rounds; avg {sum(steps) / len(steps):.2f} steps/round; 30-step limit exceeded: {step_limit_exceeded:.2f}%"
38+
)
39+
3540
# Plot CDF
3641
plt.figure(figsize=(10, 6))
3742
for model, steps in model_to_steps.items():
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import json
2+
import re
3+
4+
from matplotlib import pyplot as plt
5+
from tqdm.auto import tqdm
6+
7+
from codeclash.constants import LOCAL_LOG_DIR
8+
9+
OUTPUT_FILE = "cdf_thought_length_per_round.png"
10+
11+
12+
def main():
13+
model_to_steps = {}
14+
15+
tournaments = [x.parent for x in LOCAL_LOG_DIR.rglob("game.log")]
16+
for game_log_folder in tqdm(tournaments):
17+
with open(game_log_folder / "metadata.json") as f:
18+
metadata = json.load(f)
19+
try:
20+
p2m = {x["name"]: x["config"]["model"]["model_name"].strip("@") for x in metadata["config"]["players"]}
21+
for model in p2m.values():
22+
if model not in model_to_steps:
23+
model_to_steps[model] = []
24+
except KeyError:
25+
continue
26+
27+
for name in p2m.keys():
28+
traj_files = (game_log_folder / "players" / name).rglob("*.traj.json")
29+
for traj_file in traj_files:
30+
with open(traj_file) as f:
31+
traj = json.load(f)
32+
for message in traj["messages"]:
33+
if message["role"] != "assistant":
34+
continue
35+
content = message.get("content", "")
36+
37+
# Extract THOUGHT section
38+
thought_match = re.search(r"THOUGHT:(.+?)```bash", content, re.DOTALL | re.IGNORECASE)
39+
if not thought_match:
40+
continue
41+
42+
thought = thought_match.group(1).strip()
43+
thought_length = len(thought.split())
44+
model_to_steps[p2m[name]].append(thought_length)
45+
46+
# Plot CDF
47+
plt.figure(figsize=(10, 6))
48+
for model, thought_length in model_to_steps.items():
49+
sorted_steps = sorted(thought_length)
50+
yvals = [i / len(sorted_steps) for i in range(len(sorted_steps))]
51+
plt.step(sorted_steps, yvals, label=model, where="post")
52+
53+
plt.xlim(0, 500)
54+
plt.xlabel("Thought Length (in Words) per Round")
55+
plt.ylabel("Cumulative Probability")
56+
plt.title("CDF of Thought Length (in Words) per Round by Model")
57+
plt.legend()
58+
plt.grid(True)
59+
plt.savefig(OUTPUT_FILE)
60+
print(f"Saved CDF plot to {OUTPUT_FILE}")
61+
62+
63+
if __name__ == "__main__":
64+
main()

0 commit comments

Comments
 (0)